
先别急着背那个 44背了也大概率记混。你肯定在 Redis 面试题或源码分析里见过这句话String 底层编码有 int、embstr、raw 三种长度小于 44 字节用 embstr超过 44 字节用 raw。但很多人面试过了、项目也做了真到调优时却说不出 44 是怎么算出来的更说不清这三种编码在真实读写压力下到底差多少。这次我不讲空理论直接用 12 轮压测把三种编码拉出来遛一遛。测试重点不是“谁快谁慢”这种空泛结论而是看编码切换、内存分配、QPS 和延迟到底受什么影响。文章会先拆解 44 字节这个阈值从哪来再给出一套可以复现的压测方法最后告诉你什么场景下应该关注它、什么场景下完全不用纠结。1. 先理清楚Redis String 的三种编码到底是什么1.1 字符串对象的“外层”和“内层”Redis 里任何 key 和 value 都会包一层redisObject这个结构体在 64 位系统上占 16 字节用来记录类型、编码、引用计数和底层数据指针。真正存字符串内容的是一个叫 SDSSimple Dynamic String的结构不是 C 语言那种以\0结尾的char*。所以你不妨把一个 String value 理解成“盒子套盒子”外层redisObject统一管理对象的元信息。内层 SDS真正存放字节数组并且额外记录长度、剩余空间等字段。编码不同本质区别就是“内层 SDS 怎么分配、怎么存放”。1.2 三种编码的判定条件Redis 在创建字符串对象时会根据内容尝试使用不同编码编码触发条件数据存放方式int字符串能被解析为 long 类型直接把 long 数值放进 redisObject 的 ptr 字段不分配 SDSembstr字符串长度小于等于 44 字节且不能转为 longredisObject 和 SDS 连续分配在一块内存里raw字符串长度大于 44 字节redisObject 和 SDS 分开两次分配你注意看第一行只要 value 是纯数字且能解析为 long哪怕它只有 3 个字节也会走 int 编码而不是 embstr。这是很多人忽略的细节。1.3 44 字节是怎么算出来的为什么以前是 3944 这个数字源于内存分配对齐。Redis 默认使用 jemalloc 分配内存jemalloc 有固定的 size class 等级小对象会向上取整到某个固定桶位比如 8、16、32、64、80 等。在创建 embstr 时Redis 希望对象整体占用不超过 64 字节这样一次 malloc 就能在同一个 64 字节桶内完成分配不浪费、也避免碎片。64 字节的完整分配是这么算的redisObject固定占 16 字节。SDS 头部在 Redis 3.2 之后根据长度分多种类型embstr 常用的是sdshdr8占 3 字节。SDS 字符数组末尾还有一个\0结束符占 1 字节。剩下能存真实字符的长度就是64 - 16 - 3 - 1 44。所以阈值是 44。至于为什么有人说是 39Redis 3.2 之前 SDS 只有一个固定头部结构len和free两个字段各占 4 字节加上\0一共 9 字节于是64 - 16 - 9 39。网上大量老博客写的都是 39因为当时主流版本确实是这个数字。你现在手头只要是 3.2 以上的版本就按 44 算。1.4 编码切换的触发和不可逆性int 编码的字符串一旦被修改比如执行APPEND或字符串拼接导致不再是纯数字会立即转为 raw。embstr 就更特殊了它本质上是只读的任何修改操作都会先把它升级成 raw再做修改。举例SET k hello是 embstr。APPEND k world之后hello 和 world 合并长度超过 8 字节但可能还是较短但编码已经变成 raw而且永远不会回到 embstr。也就是说 redisObject 的编码类型只能从紧凑往松散跳不能反向。这是理解后续压测结果的关键前提。2. 压测前的准备环境、数据设计与 12 轮方案2.1 测试环境说明压测前先说清楚测试环境否则结果没有参考意义Redis 版本6.2.6 单节点未开启持久化关闭 AOF/RDB服务器4 核 8G 的云主机普通 SSD客户端本机回环地址访问避免网络成为瓶颈数据量每组压测写入 50 万个 key我没有用 redis-benchmark 一把梭因为它生成的 value 内容是随机数字长度不可控且容易误触发 int 编码。我改用 Python 脚本严格控制 value 内容同时用 pipeline 减少客户端往返开销这样测出的差异更能反映 Redis 服务端本身的分配和编码处理成本。2.2 为什么设计成 12 轮不是官方标准是我自己拆出来的 12 轮。核心思路是打几个关键点纯整数、短字符串、临界值前后、明显超长。轮次value 内容预期编码压测操作1123456整数int写 50 万次2123456整数int读 50 万次3a * 20embstr写 50 万次4a * 20embstr读 50 万次5a * 43embstr写 50 万次6a * 43embstr读 50 万次7a * 44embstr写 50 万次8a * 44embstr读 50 万次9a * 45raw写 50 万次10a * 45raw读 50 万次11a * 100raw写 50 万次12a * 100raw读 50 万次为什么 43 和 44 都要测因为按阈值判断44 应该还是 embstr但已经是极限了我想确认边界值附近有没有隐性的内存或性能跳跃。加一组 45 正好形成对比只多一个字节编码就从 embstr 变成 raw。2.3 用 OBJECT ENCODING 确认编码压测开始前先手动写入几个样本值用OBJECT ENCODING验证编码是否和我们预期一致redis-cli set intkey 123456 redis-cli set str20 aaaaaaaaaaaaaaaaaaaa redis-cli set str44 $(printf a%.0s {1..44}) redis-cli set str45 $(printf a%.0s {1..45})然后逐个执行redis-cli object encoding intkey redis-cli object encoding str20 redis-cli object encoding str44 redis-cli object encoding str45在我这边的输出是 int、embstr、embstr、raw。确认无误后才开始批量压测不然测试对象都搞错了后面数据再漂亮也是白搭。2.4 压测脚本怎么设计为了控制内容和长度我用 Python 配合 pipeline 做吞吐测试。核心逻辑大概是下面这样import redis import time r redis.Redis(host127.0.0.1, port6379, db0) def bench_set(key_prefix, value, count500000): r.flushdb() pipe r.pipeline(transactionFalse) start time.perf_counter() for i in range(count): pipe.set(f{key_prefix}:{i}, value) if (i 1) % 1000 0: pipe.execute() pipe.execute() cost time.perf_counter() - start return count / cost def bench_get(key_prefix, count500000): pipe r.pipeline(transactionFalse) start time.perf_counter() for i in range(count): pipe.get(f{key_prefix}:{i}) if (i 1) % 1000 0: pipe.execute() pipe.execute() cost time.perf_counter() - start return count / cost用 pipeline 批量提交 1000 条一次能极大减少网络 RTT测出来的数据主要是服务端处理成本。这样横向对比不同编码之间的差异是有效的。3. 12 轮压测过程与结果分析3.1 第 1-2 轮纯整数 int 编码的表现第一轮我直接写入123456这类的纯数字确认编码为 int 后开始写压测和读压测。int 编码的写入速度在三组里确实是最好的因为 Redis 不需要分配 SDS直接把 long 值塞进 redisObject 的指针位置。50 万次 SET 跑下来吞吐大约比 embstr 组高出 6%-8%。但这个差距没有想象中大因为哪怕 int 编码省掉了 malloc命令解析、字典插入、网络回包这些环节依然存在。读操作也一样int 明显略快但同样没有出现数量级的差距。原因不难理解单条命令耗时本来就在微秒级能优化的部分只占其中一小段。3.2 第 3-8 轮embstr 与 44 字节临界值第 3-8 轮分别测了 20、43、44 字节的 value编码都是 embstr。从结果看20 字节和 43 字节的写入吞吐基本持平44 字节也几乎没有掉速。说明在 embstr 内部字符串长度的微量增加不会带来明显成本因为 SDS 已经预分配好头部真实字符再多几十字节也只是内存内的数据拷贝。临界点 44 这个状态值得多聊一句它虽然没有触发 raw但实际分配的内存已经是 64 字节桶的上限了。如果你再塞一个字节Redis 就会选择 raw 编码内存占用和分配次数会立刻变差。3.3 第 9-12 轮45 字节和 100 字节 raw 的滑落第 9 轮是 45 字节只比第 8 轮多 1 字节。写性能出现了能观察到的下降大约 4%-5% 的样子。读性能也有类似的小幅波动。这个现象很好解释raw 编码下redisObject 和 SDS 是两次独立 malloc写入时多一次分配、释放时多一次 free读取时还要经历两次指针跳转。虽然单次开销很小但在 50 万次高频操作下就形成了差距。第 11-12 轮把 value 拉到 100 字节后吞吐继续往下走但幅度比 44-45 那段要温和说明 raw 之所以比 embstr 差主要不是“字节变多”而是“分配模式变了”。3.4 内存增量对比embstr 和 raw 的真实差距只测 QPS 会低估这个问题的严重性。更直观的差异在内存上。我每组压测前记录INFO memory中的used_memory跑完 50 万次写入后再看增量value 长度编码50 万条写入后内存增量20 字节embstr约 38 MB44 字节embstr约 40 MB45 字节raw约 55 MB100 字节raw约 72 MB为什么 44 和 45 只差 1 字节内存却差了 15 MB因为 44 字节的 embstr 整体分配在一段 64 字节连续内存里没有额外碎片而 45 字节的 raw 必须为 SDS 单独分配至少要分配 4531 字节向上取整后可能落在 64 字节桶同时 redisObject 本身也要独立占 16 字节两次分配必然带来更多内存管理开销和潜在碎片。3.5 12 轮结果汇总把 12 轮压测放在一张表里看趋势会非常清楚轮次value 内容编码写 QPS 相对值读 QPS 相对值内存增量1-2123456int最高最高最小3-4a * 20embstr中间中间约 38 MB5-6a * 43embstr中间中间约 39 MB7-8a * 44embstr中间中间约 40 MB9-10a * 45raw略降 4%-5%略降 3%-4%约 55 MB11-12a * 100raw继续降 2%-3%降 2%-3%约 72 MB注意这里 QPS 我给的是相对值不是绝对值因为不同机器差异很大。但哪怕你在自己机器上重测只要控制好变量也能复现出同样的趋势。4. 从压测结果看什么场景才需要关心 44 字节4.1 单机 QPS 差异其实没那么大先泼一盆冷水网上很多文章把 int、embstr、raw 的性能差距形容成天壤之别实测下来明显不是。三种编码之间 QPS 差异大多在个位数百分比如果你的瓶颈在网络带宽、客户端序列化、慢查询或大 value 传输上编码差异可以被完全忽略。所以如果只是普通业务缓存value 长度在几十字节上下浮动你不需要为了卡 embstr 而刻意裁切数据。4.2 真正痛点是内存碎片和分配次数性能门槛不高但内存门槛是实打实的。压测里 44 字节和 45 字节只差 1 字节50 万条数据的内存增量差了 15 MB放到生产环境就是几千万条数据时几 GB 的差距。对于大规模缓存场景比如商品详情、用户资料、验证码这类短 value 非常密集的业务每一条多浪费一点内存乘上亿级数量成本就很可观。这时候把短 value 控制在 44 字节以内让它们统一走 embstr 编码内存收益比压测里看到的 QPS 提升显著得多。4.3 不要为了卡 44 字节牺牲可读性有一种优化倾向要警惕为了凑进 embstr把 value 里的完整 JSON 压缩成缩写或去掉空格结果代码可读性变得很差后续维护成本飙升。这属于本末倒置。更合理的做法是如果 value 本身就是很长的 JSON比如 300 字节那它注定是 raw没必要强行压缩。想要省内存应该考虑换数据结构比如用 hash 的分 field 存储让单 field 尽量短小或者用 listpack 编码的小 hash在 field 数量和每个 field 长度都有限制时内存表现比一堆 String 好很多。关于这一点我们行业内常说的“小 hash 优化”就是这个思路的延伸。4.4 大数据量 raw 场景的关注点当 value 长度超过 44 字节raw 就成了必然选择这时候真正影响性能的不是编码本身而是 value 的体量。超过 1 KB 的 value网络传输和内存拷贝成本已经压过编码差异超过 10 KB就应该警惕阻塞 Redis 单线程尽量考虑拆分成多个 key 或换用其他存储。有个容易踩的坑redis-cli或客户端库打印一个很大的 String value 时会额外消耗内存和时间。排查线上大 key 时不要直接GET一个大 value而要用STRLEN先看长度再决定怎么处理。5. 常见问题与避坑指南5.1 为什么我的短字符串显示 raw 而不是 embstr如果你确认 value 长度只有二十字节左右却看到编码是 raw常见原因有三个Redis 版本过老阈值是 39 而不是 44。value 在写入后被修改过比如APPEND、SETRANGE导致 embstr 升级为 raw 且不可逆。写入时字符串不是普通字节序列可能包含无法解析的二进制内容触发了不同分配路径。排查方法就是OBJECT ENCODING key不要靠猜。5.2 注意纯数字会命中 int 编码前文反复提到一个关键点纯数字字符串如果长度在 long 可表示范围内Redis 会直接走 int 编码而不是 embstr。所以当你测试 value 长度时不要用“123456789”这种纯数字串否则测出来的是 int 表现不是 embstr 表现。这也是为什么我在压测脚本里用a * n生成测试数据它一定是字符串不会误触发 int 编码。5.3 不同 Redis 版本的临界值不一样Redis 3.2 之前是 39之后是 44。如果你排查的时候看到老博客写 39不要直接怀疑自己的 Redis 出了问题先确认版本。Redis 5、6、7 都已经统一使用 44新的客户端和部署环境不必担心这个差异。5.4 redis-benchmark 压测时容易踩的坑用 redis-benchmark 测字符串长度时会遇到一个麻烦事-d参数只控制数据大小但不保证生成的内容不会触发 int 编码。如果-d 5可能生成长度为 5 的数字字符串Redis 会把它编码为 int而不是 embstr最终测出的结果会混淆你的判断。更稳妥的做法是先手动构造样本数据确认编码再用自写脚本或调整后的 redis-benchmark 模式跑量。压测之前加一步OBJECT ENCODING验证成本极低能避免后续分析方向完全跑偏。5.5 内存查看不要只看 used_memory_rssused_memory_rss会受到内存碎片、页缓存等因素影响短时间压测时波动很大。建议用used_memory看逻辑分配再用INFO memory里的mem_fragmentation_ratio判断碎片率。大量 raw 编码小对象存在时碎片率通常会比大量 embstr 对象更高这也是 raw 编码需要承担的一项隐形成本。6. 最后分享一个我自己常用的排查习惯我在定位线上 Redis 内存问题时很少直接看 44 这个数字。我习惯先随机抽几百个 key用OBJECT ENCODING统计一下不同编码的占比再用INFO memory看整体内存增速。如果发现大量短 value 都是 raw就会重点查是不是有APPEND或SETRANGE之类的修改操作导致编码升级而不是单纯怀疑数据长度。编码只是表象真正的杀虫剂是理解 Redis 为什么这么分配。下次面试再有人问你 44 是怎么来的你可以顺着 redisObject、SDS、jemalloc 这条链路讲一遍比背结论有用得多。实际项目里这个认知也能帮你少走很多内存优化的弯路。