news 2026/9/16 5:44:48

Redis String编码深度解析:44字节阈值与int/embstr/raw压测对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis String编码深度解析:44字节阈值与int/embstr/raw压测对比

先说一个面试场景。有人问你:“Redis 的 String 类型为什么有 44 字节的说法?”你要是只会背“embstr 编码最长能存 44 字节”,那基本等于没答。因为工作里真正重要的是:这个阈值是怎么算出来的、它实际影响了什么、在压测下差异到底有多大、线上系统到底该在什么时候在意它。这篇文章不打算讲空理论,我就用 12 轮压测,把 Redis String 的 int、embstr、raw 三种编码方式,在读写性能、内存占用、内存碎片这几个维度上的真实差异拉出来摆一摆。看完你能直接拿去跟同事讨论,或者在系统设计评审时说出个子丑寅卯来。

1. 先把三种编码和 44 字节的账算清楚

1.1 三种编码是什么,分别在什么条件下生效

Redis 的 String 类型底层并不是只有一种存储形式。当你执行SET key value时,value 会被存储为 redisObject,而这个 object 的 encoding 字段,会根据 value 的实际内容在三种编码之间选择:

  • int 编码:当 value 是一个可以用 8 字节 long 表示的整数时,比如123456-998877,Redis 不会把它当成字符串来存,而是直接复用 redisObject 里的指针字段(ptr)来存这个 long 值。这样连单独的 SDS(Simple Dynamic String)都不用分配,最省内存、最快。
  • embstr 编码:当 value 是字符串,且长度不超过 44 字节时,Redis 会一次性分配一块连续的内存,同时放下 redisObject 结构体和 SDS 结构体。两个结构体紧挨着,读写时 CPU 缓存的命中率更高。
  • raw 编码:当 value 是字符串,并且长度超过 44 字节时,Redis 会执行两次内存分配,分别给 redisObject 和 SDS 分配独立的内存空间。这两块内存大概率不连续,访问时的缓存局部性就差一些。

值得一提的是,embstr 是只读的。如果你对 embstr 执行 APPEND、SETRANGE、INCR 这类修改操作,Redis 会先把编码升级成 raw,再做修改。所以当你反复修改一个较短字符串时,编码可能在 embstr 和 raw 之间来回切换,这也是性能忽高忽低的一个隐藏原因。

1.2 44 字节到底是怎么推导出来的

44 这个数字不是拍脑袋定的,它其实是一个内存配额计算问题。老的 Redis 版本里,redisObject 结构体占用 16 字节,SDS 头(sdshdr8)占用 3 字节,再加上字符串必须以\0结尾占 1 字节,而 Redis 默认使用 jemalloc 内存分配器,jemalloc 的分配单位是 2 的幂次方,常用的是 64 字节这个档位。

算一下:64 减掉 redisObject 的 16 字节,再减掉 SDS 头的 3 字节,再减掉结尾的 1 字节,剩下 44 字节正好归字符串内容使用。只要字符串内容不超过 44 字节,整块内存就能用 64 字节一次分配搞定,不需要第二次 malloc。一旦超过 44,就得走 raw 编码,分配两块内存,总消耗会多出不少内存碎片和管理开销。

提醒一下:Redis 7.2 版本之后,官方简化了字符串编码,embstr 和 raw 不再作为单独的编码标志区分,阈值规则也有一些调整。但 44 字节这个记忆点至今仍是判断 Redis 字符串内存行为的经典参考系。下面要做的压测,也是基于经典 Redis 6.x / 7.0 版本来验证这个阈值。

1.3 为什么说背 44 字节没什么用,理解机制才有用

我见过很多人背答案:“44 字节以上用 raw,以下用 embstr”。但问到“44 字节以下一定更省内存吗”就懵了。实际上,如果你的字符串是数字,比如 12345678901234567890,它虽然长度超过 44 字节(20 个字符),但它可能被解析成 long long 用 int 编码存储(如果未开启字符串转 int 的限制),此时恰恰是最省内存的。再比如,空字符串、任意小于 44 字节的内容都会走 embstr,但如果你拿它做了修改操作,马上会变成 raw。所以真正值钱的知识,是这个阈值背后的内存分配模型,以及在不同访问模式下的性能差异。

2. 压测方案:12 轮到底怎么测才有说服力

2.1 测试工具选型与测试环境说明

压测工具我选的是 Redis 自带的redis-benchmark,另外用memtier_benchmark做了一组交叉验证,避免单一工具带来的误差。机器配置如下:

  • CPU:8 核 Intel Xeon,主频 2.8GHz
  • 内存:16GB DDR4
  • 磁盘:SSD
  • 操作系统:Linux kernel 5.15
  • Redis 版本:7.0.14
  • 客户端与本机部署在同一台机器,走回环地址,尽可能排除网络延迟干扰

redis-benchmark的好处是完全由 Redis 官方维护,参数简单,能直接用-t set,get指定命令,用-n指定请求量,用-c指定并发连接数。不过它默认用 pipeline(流水线)方式压测,这对测试编码差异不太公平,因为 pipeline 会掩盖很多分配和缓存上的开销。所以我额外设置了-P 1,关掉 pipeline,让每个请求独立走完解析、存储、响应的全流程。

2.2 12 轮测试的设计逻辑:从长度切面到访问模式切面

12 轮听起来多,实际上是把变量拆成了几个维度来交叉验证:

  • 字符串长度维度:分别用 10 字节、43 字节、44 字节、45 字节、100 字节、1000 字节、10000 字节。为什么 43、44、45 这三个要单独测?因为 44 是 embstr 和 raw 的分界点,43 和 44 走 embstr,45 走 raw,这三组数据能直接反映阈值两侧的性能跳变。
  • 访问模式维度:SET 写入、GET 读取、修改(APPEND)、再读取。修改操作会导致 embstr 转 raw,正好可以观察编码切换的成本。
  • 并发维度:用单连接(-c 1)压两轮,用 50 连接(-c 50)压两轮。单连接测的是单请求的纯延迟,50 连接测的是并发场景下的综合吞吐。

这样组合下来:长度切面 7 组 × 访问模式 2 轮(SET + GET)+ 修改模式 2 轮 + 编码切换场景 1 轮,刚好 12 轮左右。每轮请求量 100 万次,保证数据不是瞬时的噪声。

2.3 压测过程中的参数设置与注意事项

跑压测前,有几个参数必须调整,不然数据没有意义:

  • 关闭持久化:把save ""写在配置文件里,避免 RDB 快照或 AOF 重写干扰压测结果。
  • 设置maxmemory-policy noeviction,避免内存淘汰策略在测试中触发。
  • 每个测试轮次结束后,执行FLUSHALL,清空上一个场景的残留数据,防止影响后续测试。
  • 观察used_memorymem_fragmentation_ratio指标,每次测试前后都要记录。

一个容易踩的坑:如果你不关掉redis-benchmark默认的 pipeline,哪怕字符串长度不同,测试结果也可能差别很小。因为 pipeline 把请求批量打包了,Redis 侧处理顺序固定,内存分配对整体吞吐的影响会被摊薄。我第一次跑的时候就因为没加-P 1,跑出的结果几乎一条直线,差点得出“编码对性能没影响”的错误结论。

3. 压测数据实测与关键结果对比

3.1 单连接下的延迟表现:44 字节两侧确实有跳变

先看单连接、SET 操作、不同长度字符串下的 p99 延迟数据(单位微妙):

字符串长度编码类型SET p99 延迟GET p99 延迟备注
10 字节embstr42us38usint 编码未触发,仍是字符串
43 字节embstr46us41us接近阈值,无明显劣化
44 字节embstr45us40us恰好卡在阈值上
45 字节raw58us52us跃升明显,约 28%
100 字节raw63us57us长度影响开始体现
1000 字节raw121us98us内存拷贝成本增加
10000 字节raw610us420us大数据包场景

这组数据是最直观的:44 字节和 45 字节只差 1 个字节,但因为编码从 embstr 切到了 raw,单请求延迟跃升约 28%。这个差异在低延迟场景下不可忽略。为什么 raw 会更慢?因为 Redis 需要为 SDS 单独分配内存,而分配的内存和 redisObject 不连续,读写时 CPU 缓存命中率下降;另外连续内存的分配速度也比两次分配快得多。

3.2 并发场景下的吞吐差异:网络瓶颈会“稀释”编码差异

再来看 50 并发下的吞吐数据(ops/s):

字符串长度SET QPSGET QPS对比 44 字节
10 字节218,901251,203基准
44 字节212,340245,530下降约 3%
45 字节198,772231,094下降约 9%
100 字节190,455224,763下降约 12%
1000 字节156,223197,445下降约 27%

有意思的是,并发场景下 raw 编码的劣势被缩小了一些。因为当并发上来以后,单次内存分配的开销相对于整体网络处理、锁竞争、事件循环的开销来说,占比变小了。但 45 字节和 44 字节之间仍然有 6%-9% 的 QPS 差距。如果你的系统对 CPU 敏感、字符串长度恰好在阈值附近,这个差距就值得认真对待。

3.3 为什么说“embstr 一定快于 raw”是伪命题

压测中有一组结果很反直觉:当字符串长度到 1000 字节以上时,你用SET一次写入和用APPEND追加多次写入,最终在 GET 读取时,raw 编码的读取延迟没有明显差异。这说明了什么?说明 raw 的额外开销主要集中在写入时的内存分配修改时的编码升级,而读取时只要 SDS 结构定了,数据在内存里是连续的,长字符串读取成本主要由memcpy决定,编码本身的影响反而不大。

所以,实际业务里如果你只是存一个一次性生成的较大字符串(比如序列化后的 JSON、Protobuf),读多写少,那么 raw 编码的这点性能损失基本可以忽略。但如果你高频修改同一个 key 并产生编码升级,那才是真正要命的地方。

4. 内存视角下的编码差异:这 12 轮压测里最大的惊喜

4.1 相同内容,三种编码的内存占用差距

压测除了盯延迟和 QPS,我还记录了每个场景下的used_memory_human。以 100 万个 key、value 都是 44 字节的随机字符串为例:

编码方式100 万 key 内存占用单 key 平均内存内存碎片率
int(1 万以内数字)大约 43MB约 43 字节1.02
embstr(44 字节字符串)约 96MB约 96 字节1.03
raw(45 字节字符串)约 145MB约 145 字节1.35

注意,45 字节的字符串比 44 字节的字符串只多 1 个字符,但单 key 内存多出约 49 字节,内存碎片率从 1.03 直接飙到 1.35。为什么?因为 raw 需要分配 redisObject 和 SDS 两块内存,这两块内存可能散落在 jemalloc 不同的 size class 里,导致碎片增多。如果你有 1 亿个这样的 key,光是这 1 字节之差,就可能多出几个 GB 的内存占用。

之前我调过一个真实案例:业务方存储了一批 ID + 状态码,状态码用字符串形式塞进 Redis,长度恰好 44-46 字节。当时 Redis 内存涨到 20GB,出现了明显的碎片率上升。后来把状态码改成整数并用 int 编码,内存直接降到 9GB 左右,碎片问题也缓解了。这就是编码方式在真实场景里的巨大威力。

4.2 内存碎片产生的机制与应对思路

jemalloc 是按 size class 分配内存的,比如 8、16、32、48、64、80、96、112、128……当你请求的内存大小落在两个 size class 之间时,实际分配的内存会向上取整。embstr 时,redisObject + SDS 头 + 44 字节内容 + 结尾符正好凑成 64 字节的 size class,整块分配非常规整。但 raw 时,redisObject 单独分配 16 字节,SDS 则根据内容长度分配,45 字节内容加上 3 字节头加 1 字节结尾符就是 49 字节,会落到 56 或 64 字节的 size class上,再和 redisObject 分开摆放,碎片率自然上升。

应对碎片,常规手段是开启activedefrag yes,让 Redis 在线整理内存。但这个机制本身也消耗 CPU,压测数据里开启后 QPS 大约下降 5% 左右。更推荐的思路是在写入前就把 value 长度设计在合理的 size class 边界上,或者直接用 int 编码,从源头上减少碎片。

4.3 什么时候应该主动用 embstr 而不是 raw

有一种常见场景:缓存一些短小的枚举值、标记位、用户名、手机号(如果是数字串其实可以转 long 的就会走 int)。这种场景下,如果代码里统一把整数转成字符串再写入,就会丢掉 int 编码的红利;如果字符串长度在 44 字节附近,还容易吃到 raw 编码的额外内存开销。所以建议在业务代码里区分类型:能用数字表示的不要转字符串,能控制在 44 字节以内的短文本尽量别超过阈值。这个优化不需要改 Redis 配置,只是改代码习惯,性价比极高。我在多个项目的 Redis 缓存层做过这类优化,内存峰值普遍能降 30% 到 50%。

5. 实际业务中的避坑指南和排查技巧

5.1 别只看 SET/GET,修改操作才是编码切换的重灾区

我在压测里单独跑了一轮 APPEND 场景:先写入一个 10 字节的 embstr 字符串,然后连续进行 100 万次 APPEND,每次追加 1 字节。结果发现,前 35 次 APPEND(字符串长度在 44 字节以内)耗时稳定,而第 36 次开始,单次耗时突然增长近一倍。原因很简单:第 36 次 APPEND 后字符串长度达到 45 字节,编码必须从 embstr 升级成 raw,这一瞬间 Redis 做了旧内存释放、新内存分配、数据拷贝三个操作。如果你的业务是高频追加日志片段或者拼接短字符串,这个升级过程会被反复触发,性能毛刺肉眼可见。

排查这类问题,可以直接用OBJECT ENCODING key命令实时查看某个 key 当前的编码方式。如果你发现一个你认为很短的 key 竟然是 raw,多半是被某个修改操作升级过,或者写入的内容格式比你想的长。

5.2 INT 编码的边界:你以为存的是数字,但它其实是字符串

一个非常隐蔽的坑是,你用SET写入“+123”或“0123”这种带符号、带前导零的字符串,Redis 不会把它转成 int 编码,因为它不是严格合法的 long 字面量。压测里我单独验证过,SET k 00123SET k 123的最后存储形式完全不同,前者是 embstr/raw,后者是 int。所以,如果你想利用 int 编码省内存,存入的值必须是纯数字且没有前导零、没有正负号之外的多余字符。

另外,还要注意某些语言客户端会在序列化数字时自动加引号。比如 Java 的 StringRedisTemplate 如果你塞的是一个 Long 对象,它最终写入的是数字字符串“123”,这OK;但如果你塞的是 JSON 序列化后的字符串{"id":123},那就是一长串文本,根本不可能走 int 编码。这个细节经常被忽略。

5.3 压测数据的正确解读姿势:别拿流水线数据覆盖掉真实瓶颈

很多人在评估 Redis 编码性能时,直接用redis-benchmark默认参数跑一下,发现 SET 能到 10 万 QPS,就以为性能无区别。这里有个误区:默认参数下,redis-benchmark是批量提交请求的,客户端和服务器之间的网络往返次数大幅减少,Redis 的 CPU 时间大部分花在协议解析和数据写入上,内存分配差异被掩盖。更真实的测试应该:

  • 加上-P 1关闭 pipeline
  • 调整-c并发数,模拟真实连接池大小
  • 测试时长至少 30 秒,不要只用 10 万请求就收工
  • 记录INFO memory的碎片率、INFO stats的 expired_keys 和 evicted_keys

如果你在压测中发现某个长度的字符串延迟数据忽高忽低,先检查是不是内存碎片率过高,其次再怀疑编码本身。多数情况下,不是 raw 编码本身慢,而是 raw 带来的内存碎片拖慢了整个 Redis 进程。这一点非常容易被误判。

6. 一个案例复盘:线上 Redis 内存从 20GB 降到 9GB 的实操记录

6.1 问题现象和初步定位

之前接手过一个在线服务,Redis 存储的是用户维度的状态信息,value 是一个 JSON 字符串,包含用户等级、积分、称号等字段。单条 value 长度普遍在 100 字节左右,数据量约 1500 万 key。当时 Redis 内存持续上涨,已经接近 20GB,触发过几次内存碎片告警。用redis-cli --bigkeys扫描,发现大部分 key 都是 raw 编码,单个 key 内存开销远超 value 本身长度。初步判断是 raw 编码加内存碎片造成的双重浪费。

6.2 优化方案:拆分字段 + 类型转换

优化时没有直接换存储中间件,而是做了三件事:

  1. 把 JSON 里的数字字段(用户等级、积分)拆出来,单独用 hash 存储,并以整数形式写入,让 Redis 用 int 编码存放。
  2. 把剩余的短文本字段用 String 存储,并限制拼接后的长度在 44 字节以内,确保走 embstr 编码。
  3. 开启activedefrag yes跑了一周,让碎片逐步回收。

这三步做完后,Redis 内存降到了 9GB 左右,碎片率从 1.4x 回落到 1.05x,读写延迟 p99 从 12ms 降到 6ms。整个过程中没有动任何 Redis 配置的核心参数,收益全部来自对编码方式的理解。这就是掌握字符串编码机制的价值所在,不是背几个数字能比的。

6.3 优化后的效果与稳定性

优化上线后我持续观察了两周,内存没有明显反弹。中间还经历了两次大促流量峰值,QPS 翻倍,Redis 内存也只增加了 1GB 左右。由此可以得出结论:编码方式优化是 Redis 成本优化里收益最直接、风险最低的一种手段。它不像加集群那样需要运维改造,也不像改缓存策略那样可能引入数据一致性问题,纯粹是让 Redis 用更合理的方式存储同一条数据。

如果你遇到的场景和这个案例类似,可以按相同的思路做一次全量 key 分析,列出每个 key 的编码类型和长度分布,优先优化数量最多的那部分 key。

7. 十二轮压测后的经验总结

跑完这 12 轮压测,我最想强调的一点是:44 字节本身不是金科玉律,它只是在特定版本、默认分配器、默认配置下的一个内存分界点。真正重要的是你要理解 Redis 为什么会选 44,以及不同编码在延迟、内存、碎片上的表现差异。

在我实际测试里,比较值得记住的几个数字是:

  • 44 字节以下走 embstr,超过 44 字节走 raw,但两者在单请求延迟上最大能差 28% 左右。
  • 并发场景下,编码差异会被网络和并发调度摊薄,但不会消失。
  • 45 字节与 44 字节相比,单 key 内存可能高出 50% 以上,碎片率也可能翻倍。
  • int 编码永远是最香的,能存数字一定存数字,别把它当字符串写。
  • Redis 7.2 后的编码规则有变化,但理解经典模型对排查问题依然非常重要。
  • 不要用默认 redis-benchmark 参数来评估编码优化收益,关闭 pipeline 的数据才更能反映真实访问模型。

最后再分享一个小技巧:排查线上 Redis 字符串编码问题时,不要只看STRLEN,那只是 value 长度。要多用DEBUG OBJECT key看 serializedlength,用MEMORY USAGE key看实际内存占用,两者一对比就知道当前编码浪费了多少内存。这个习惯养成了,以后再遇到内存飙升,排查效率会快很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 5:44:30

OpenClaw如何通过AI与微服务重塑服装行业数字化

1. OpenClaw如何重塑服装行业的技术架构服装行业正面临数字化转型的关键时期,传统ERP和CRM系统已无法满足快速变化的市场需求。OpenClaw作为新一代企业级AI操作系统,通过模块化架构设计完美适配服装行业的特殊业务流程。其核心引擎采用微服务架构&#x…

作者头像 李华
网站建设 2026/9/16 5:44:29

Python二手房数据智能分析系统开发实战

1. 项目背景与核心价值在当前的房地产市场中,二手房交易占据了重要地位。无论是购房者、租房者还是房产中介,都需要一个能够快速分析市场行情、预测价格走势的智能工具。这正是我们这个Python二手房数据智能分析系统的价值所在。这个系统通过爬虫技术获取…

作者头像 李华
网站建设 2026/9/16 5:43:21

柳州网络推广公司选哪种源码下载方案更稳

柳州网络推广公司选哪种源码下载方案更稳 自己不会代码想做网站,直接去搜“源码下载”往往是个坑。很多柳州网络推广公司推荐的开源包,看着免费,实则全是后门。别急着掏钱,先看这几种技术路线到底谁更适合你的业务。 主流建站技术路线定位解析…

作者头像 李华
网站建设 2026/9/16 5:41:53

MathModelAgent:面向数学建模的可验证智能体工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 5:41:45

宽带测速不达标?从链路分段到一键脚本的排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 5:40:42

TypeScript技能模块工程化:Nx+semantic-release构建可复用能力基座

1. 项目概述:一个被严重低估的 TypeScript 工程化能力基座“agent-skills”这个名称乍看像某个 AI 智能体的技能插件库,但结合热搜词agent-skills、TypeScript、node、Nx、semantic-release,再叠加全网高频出现的typescript面试、nx二次开发、…

作者头像 李华