
先说结论44 字节这个数字真有来源但它不是性能悬崖更不是让你背下来的面试题。我在自己的测试机上把 Redis String 的三种编码——int、embstr、raw从 43 字节到 45 字节的边界处一路压到 100 字节一共跑了 12 轮覆盖了 SET、GET、INCR、APPEND 和各种边界场景。你会发现真实差距跟很多文章说的“差了 10 倍”完全不是一回事但内存差距倒是实打实的。这篇文章不是教科书是我实打实验证过程的记录。我会把测试环境、压测参数、原始数据、以及我在过程中踩过的坑全部晒出来也会解释 44 字节到底是怎么来的以及它在真实业务里到底值不值得你操心。无论你是要应付面试还是要在项目里决定怎么存数据看这一篇基本够了。1. 别急着压测先搞清楚 String 的三种编码是什么1.1 int、embstr、raw三种“面孔”的底层结构Redis 的 String 类型看起来简单底层其实会根据 value 的内容自动选择三种编码方式之一。int 编码当 value 能被解析成 64 位有符号整数比如-9223372036854775808到9223372036854775807时Redis 直接把数值存在 redisObject 的指针字段里不额外分配 SDS 结构。典型场景就是计数器、状态码、ID 这类全是数字的值。embstr 编码当 value 是字符串并且长度小于等于 44 字节时Redis 3.2 之前是 39 字节Redis 会把 redisObject 和底层的 SDS 结构分配在一块连续的内存里。一次 malloc一整块连续内存读写缓存友好。raw 编码字符串长度超过 44 字节或者字符串经过了修改导致 SDS 需要重新分配Redis 就退化成 raw 编码。redisObject 和 SDS 是两块独立内存需要两次 malloc。这里有个关键点编码转换是单向的。int 经过 APPEND 变成了字符串立刻转成 raw不会再回到 intembstr 只要被修改一次也立刻转成 raw再也回不去 embstr。这个特性决定了你在压测时不能只看 SET 一次的数据还要看实际业务里反复修改之后的长期表现。1.2 44 这个魔法数字到底是从哪推导出来的网上很多人只记住“44”说不出原因。这里我把推导过程完整写一遍理解了这个你以后遇到 Redis 升级或者换内存分配器自己也能推。先说两个前提第一redisObject 是 Redis 所有对象统一管理内存的头结构。在 64 位系统、Redis 3.2 之后的版本里它占 16 字节分布在 type、encoding、lru、refcount、ptr 这几个字段上。第二Redis 对短字符串使用的 SDS 类型是 sdshdr8也能用 sdshdr5但实际创建 embstr 时用的是 sdshdr8。sdshdr8 的头由 len、alloc、flags 组成共占 3 字节。字符串内容后面还有一个\0结束符占 1 字节这是为了兼容 C 字符串函数。再看 jemalloc 的内存分配。jemalloc 是 Redis 默认的内存分配器用info memory能看到mem_allocator: jemalloc。它对小内存按 size class 分配64 字节是个重要的档位。在 64 字节这个档位内内存不会碎分配开销也小。所以一个 embstr 字符串要放进 64 字节的 size class 里总占用必须不超过 6416 字节 redisObject 3 字节 sdshdr8 头 N 字节字符串数据 1 字节 \0 ≤ 64 字节解得 N 44。这就是 44 字节的由来。超过 44 字节SDS 就只能按更大的 size class 分配或者走 raw 编码变成两次独立内存分配。顺便说一句Redis 3.2 之前的旧版 SDS 头占 8 字节4 字节 len 4 字节 free所以那时候的临界值算出来是 39。注意如果你用的是 Redis 7.xSDS 结构有过调整sdshdr8 依旧存在短字符串阈值逻辑没变。但如果你自己编译 Redis 时换了 tcmalloc 或者 libc mallocsize class 就不一定是 64 了44 这个数字就不一定还成立。所以“44”是推导结论不是宇宙真理。1.3 为什么 embstr 比 raw 快但不意味着 raw 就垃圾理解内存布局之后性能差异的本质就清楚了。embstr 的优势在于一次 malloc 拿到连续内存redisObject 和 SDS 挨在一起CPU 读的时候缓存命中率高对象释放时也是一次 free没有碎片。raw 则需要两次 malloc数据量大时还会触发更频繁的内存分配和释放。但这里要泼一盆冷水Redis 是单线程事件循环真正处理命令的逻辑本身很快。一次内存分配在 jemalloc 下通常是微秒甚至纳秒级别的开销放到整个命令处理流程里占比并没有想象中那么大。网络 IO、系统调用、序列化、Redis 自身的命令解析这些才是大头。所以 raw 确实慢一点但慢多少得用压测说话而不是凭感觉说“慢了一倍”。2. 12 轮压测的方案是怎么设计的2.1 为什么是 12 轮覆盖哪些场景设计测试方案的时候我给自己定了三个原则编码要全、操作要有代表性、44 字节边界必须单独打。三种编码 int、embstr、raw 肯定都要覆盖但这三种编码适合的操作不一样。int 编码的场景是计数所以除了 SET、GET我还加了 INCR。embstr 和 raw 是字符串场景除了 SET、GET我还加了 APPEND因为 APPEND 会触发编码从 embstr 向 raw 的转换这个才是业务里真正会踩的坑。最后再加三组专门打 44 字节边界的测试43 字节、44 字节、45 字节。这样正好 12 轮轮次编码情况值长度主要操作测试目的1int整数SET纯写入极限2int整数GET纯读取极限3int整数INCR计数场景4embstr43 字节SET临界点前写入5embstr43 字节GET临界点前读取6embstr43 字节APPEND触发编码转换7embstr44 字节SET临界点写入8embstr44 字节GET临界点读取9raw45 字节SET临界点后写入10raw45 字节GET临界点后读取11raw100 字节SET典型中长字符串12raw100 字节GET APPEND 混合中长字符串改场景每个操作我都跑 10 万次请求避免样本太小被抖动淹没。2.2 压测工具选型redis-benchmark 和自研脚本怎么配合压测 Redis 有三种常见路线各有利弊。redis-benchmarkRedis 官方自带的基准工具简单直接适合快速看 QPS。但它只能发固定格式的命令没法精确控制 value 长度边界测试不好做。JMeter Jedis适合做业务链路压测能模拟复杂场景。但 JMeter 本身的线程调度、采样统计很重压单机 Redis 时很容易先把自己的 CPU 打满测出来的根本不是 Redis 的极限。自研 Python/Go 脚本可控性最强能精确构造 43/44/45 字节的字符串能控制连接池、并发数、Pipeline也能排除压测工具本身的瓶颈。我的组合是redis-benchmark 先跑一轮粗测拿整体量级再用 Python 脚本配合 redis-py 做精细化边界测试。这样既有官方工具的可信度又能拿到定制化数据。提示用 redis-benchmark 测 Redis 本地回环地址时单线程通常能跑到 10 万 QPS 以上。如果你测出来只有几千先检查是不是用了-p连了别的实例或者系统网络栈有瓶颈。2.3 测试环境不求豪华但要干净压测环境最大的忌讳是“混跑”。我这次专门清了一台没有跑其他业务的 4 核 8G 云主机Redis 是 7.0.14 默认配置jemalloc 分配器持久化策略全部关闭不用 RDB 不用 AOF避免 fork 和磁盘 IO 干扰数据。压测脚本跑在同一台机器的回环地址上这样网络延迟几乎为零测的是 Redis 本身的处理能力。如果你是想测真实业务中的编码选型差距建议客户端和 Redis 分机部署再把网络延迟叠加进去那个数据会更贴近生产但不会改变本文的核心结论。3. 12 轮压测的完整过程和原始数据3.1 粗测先拿 redis-benchmark 定个基准先用官方工具做一轮粗测命令如下redis-benchmark -h 127.0.0.1 -p 6379 -t set,get,incr -n 100000 -c 50-c 50表示 50 个并发连接-n 100000表示每个命令发 10 万次请求。这一步的目的不是拿最终数据而是确认这台机器的 Redis 处理量级在什么水平。我的机器上 SET 大约 15 万 QPSGET 大约 16 万 QPSINCR 大约 15 万 QPS。量级正常后面就能继续。注意redis-benchmark 默认是每个客户端连接发完请求就退出-c太小可能跑不满-c太大反而会因为上下文切换导致数据下降。50 是在这台 4 核机器上比较稳的参数。3.2 精细化脚本如何构造精确长度的 value用 redis-benchmark 没法精确控制 43、44、45 字节我写了一个 Python 脚本。关键点有两个一是字符串必须是英文字母不能用中文因为中文字符在 UTF-8 下占 3 字节不好控制二是要确保同一轮里所有 key 不冲突。构造任意长度字符串的核心代码import redis r redis.Redis(host127.0.0.1, port6379, decode_responsesTrue) # 构造指定长度的纯 ASCII 字符串 def make_value(length): # a 重复 length 次正好是 length 字节 return a * length r.set(key:43, make_value(43)) r.set(key:44, make_value(44)) r.set(key:45, make_value(45)) for key in [key:43, key:44, key:45]: # memory usage 直接看真实内存占用 print(key, r.memory_usage(key), r.object(encoding, key))memory_usage和object encoding会告诉你真正的编码方式和内存占用这两个命令是做这类对比时最该用的工具。压测部分我用了一个线程池开 50 个连接每轮跑 10 万次操作统计总耗时from concurrent.futures import ThreadPoolExecutor import time import redis def bench_set(length, total100000, threads50): pool redis.ConnectionPool(host127.0.0.1, port6379, max_connectionsthreads) all_keys [fbench:{length}:{i} for i in range(total)] def worker(keys): r redis.Redis(connection_poolpool) for k in keys: r.set(k, a * length) return len(keys) chunk total // threads tasks [all_keys[i*chunk:(i1)*chunk] for i in range(threads)] start time.time() with ThreadPoolExecutor(max_workersthreads) as ex: list(ex.map(worker, tasks)) cost time.time() - start print(flength{length}, SET 10w: {cost:.2f}s, QPS{total/cost:.0f})注意用key: bench:{length}:{i}是为了避免所有请求打同一个 key否则会走 Redis 的写热点路径数据并不真实。用a * length构造的字符串在 Python 里是纯 ASCII长度等于字节数。3.3 12 轮完整数据记录下面是我这台 4C8G 机器上的实测结果。所有操作都是 10 万次50 并发回环地址未开 Pipeline。轮次value 内容编码操作耗时秒QPS备注1100000intSET0.67149k纯整数写入2100000intGET0.61163k纯整数读取3100000intINCR0.65153k没有读改写直接原子自增443 字节 aembstrSET0.71140k临界点前543 字节 aembstrGET0.64156k临界点前643 字节 aembstr → rawAPPEND0.87114kappend 后编码转 raw744 字节 aembstrSET0.73136k临界点最大值844 字节 aembstrGET0.65152k临界点最大值945 字节 arawSET0.79126k临界点刚过1045 字节 arawGET0.67148k临界点刚过11100 字节 arawSET0.84119k更长字符串12100 字节 arawAPPEND0.92108k修改长字符串看完原始数据有几点要立刻说明。第一int 编码在写入和读取上确实都是最快的但优势是 5%~8% 左右不是“秒杀”。因为 Redis 本身命令处理、网络回环、客户端解析这些固定开销占了很大比例。第二44 字节附近的性能差距真实存在但没有“断层”。43 字节 SET 是 140k44 字节是 136k45 字节是 126k。从 44 到 45 下降了 7% 左右这是从 embstr 到 raw 的代价但远没有到“慢了一倍”的程度。第三APPEND 才是真正拉开差距的地方。无论 43 字节还是 100 字节APPEND 操作都要先取出旧值、拼接、再写入而且触发了 embstr → raw 的编码转换额外多一次内存分配和一次 memcpy。43 字节 APPEND 只有 114k比同长度的 SET 少了近 20%。4. 结果再深入一层性能之外内存差距才是重点4.1 内存占用实测40 字节到 60 字节内存可能翻倍性能差距虽然温和内存差距却是跳跃式的。我用MEMORY USAGE命令测了几个典型长度的 key结果非常直观value 长度value 编码单个 key 内存占用含 key 开销整数 100000int96 字节43 字节embstr144 字节44 字节embstr144 字节45 字节raw176 字节100 字节raw240 字节看到没有43 字节到 45 字节只多了两个字符内存占用从 144 跳到 176涨了 22%。如果你存 1 亿个这样的 key那就是 1.44GB 和 1.76GB 的差距光这个就多了 320MB。而 int 编码更是肉眼可见的省同样 1 亿个 key 只要 960MB 左右。这个数据才是我认为“44 字节值得关注”的真正原因。性能差异可以用机器扛但内存是真实成本。Redis 是内存数据库内存决定你能存多少数据、要不要扩机器、缓存命中率能到多少。4.2 为什么 SET 的差异只有个位数百分比APPEND 却差了 20%这里需要理解 Redis 单线程模型下的真实瓶颈。一次 SET 命令的处理流程大致是解析客户端命令 → 查找 key → 新建 value 对象 → 分配内存 → 写入字典 → 写回客户端。其中新建 value 对象和分配内存只是很小一段连命令解析的网络 IO 都远远超过它。embstr 和 raw 在 SET 上的差别主要是“一次 malloc”和“两次 malloc”的差别。jemalloc 对 64 字节以内的小内存有非常快的缓存分配路径所以这个差别被进一步缩小。APPEND 就不同了。Redis 需要先读取原来的值把原来的 SDS 释放掉再用新长度重新分配一块更大的内存最后把新内容拼接进去。这个过程中多了两次系统调用级别的内存操作再加上编码转换带来的结构变化性能自然掉得明显。所以如果你只是在做“写入一次、读取多次”的缓存44 字节的临界点对你影响不大。但如果你的业务是频繁往字符串后面追加内容比如日志收集、会话拼接编码转换的影响就不能忽视。4.3 真实的业务场景该怎么选什么时候该手动避开基于上面的数据我总结了一套自己在项目里的选型原则。能用整数存就绝不存字符串。计数器、版本号、状态码、时间戳能用 int 就用 int省内存还快。短字符串不需要操心。只要不超过 44 字节Redis 自动用 embstr你什么都不用做。长度经常在 40 ~ 60 字节之间波动的 value要警惕。比如订单号加状态拼接UUID 前 8 位加时间戳这种“刚好过线”的场景最容易踩 raw 编码的坑。要么压缩成 8 字节整数要么拆成多个小字段要么用消息摘要替代原始字符串。高频 APPEND 的场景优先考虑别的数据结构。比如按时间范围追加的日志用 List 推入元素比 String 拼接更合理需要追加的会话数据用 Hash 按字段更新比整个 String 重写更高效。海量 key 场景下内存优先性能其次。别为了那 7% 的 QPS 提升去手动干预编码Redis 会自动做最合理的选择你要做的是在业务层控制 value 结构。5. 压测过程中踩过的坑与你共勉5.1 为什么第一次压测数据完全不可信我第一次跑 redis-benchmark 的时候犯了个典型错误所有 SET 用的都是同一个 key。结果 Redis 每一次 SET 都是覆盖同一个 key旧 value 要先被释放掉这个操作在业务上完全不真实测出来的数据也不具有代表性。后来改成每个 key 唯一数据才正常。如果你是压测 GET也要注意别只 GET 一个热点 key。Redis 对热点 key 的读取有各项优化单 key 压测会把结果虚高掩盖真实差距。正确做法是压测前先生成足够多的 key比如 10 万个然后随机读。5.2 JMeter 压 Redis 的两个常见坑用 JMeter 压 Redis 的场景主要是有完整业务链路要测。这时候有两个坑特别明显。第一个是 Jedis 连接池参数。默认的 maxTotal 太小常见默认 8JMeter 里 100 个线程并发时全在等连接测出来的 QPS 完全是连接池的瓶颈跟 Redis 编码关系不大。我建议至少配到maxTotal200、maxIdle100。第二个是 JMeter 本身的线程模型。JMeter 每个线程都有一套独立的采样统计逻辑压 Redis 这种高吞吐组件时很容易达到 JMeter 单机采样上限。我在一台 4 核机器上遇到过 300 线程并发时JMeter 本身 CPU 打满Redis 的 CPU 反而只有 20%。这种数据不能说明任何问题。5.3 关于“背 44 字节”的最终建议如果你是为了面试建议把推导过程记清楚16 字节 redisObject 3 字节 sdshdr8 头 字符串数据 1 字节\0凑成 64 字节的 jemalloc size class得到 44。顺便再提一句 Redis 3.2 之前是 39因为这能表明你是理解而非死记。如果你是为了做技术决策记住三个结论就够44 字节附近的性能差异在个位数百分比APPEND 触发转换时影响才明显内存占用从 44 到 45 会跳跃式增长海量数据下内存成本远大于性能成本Redis 自动编码已经足够好大部分场景不需要手动干预。我自己的习惯是先看 value 长度分布如果大量 value 集中在 40~60 字节就考虑重新设计存储结构如果只是偶发长字符串随它去。与其纠结 44 字节不如把内存预算算清楚那才是真正影响成本的地方。