1. 一次必须较真的 Redis String:三种编码到底从哪来
做 Redis 相关项目或者准备 Redis 面试的同学,应该都碰到过“44 字节”这个数字。面试八股文里经常写:Redis 的 String 类型在存储短字符串时用 embstr 编码,当长度超过 44 字节就切换成 raw 编码。背答案很容易,但有个问题很棘手:44 这个分界线到底怎么来的?embstr 和 raw 在真实压测下性能差距有多大?int 编码为什么经常被忽略?如果只背结论,碰到内存优化、大 key 治理、缓存设计这些实际场景,你仍然会两眼一抹黑。
我这次专门花了两天时间,搭了一个相对干净的 Redis 实例环境,设计了 12 轮压测,把 int、embstr、raw 三种编码的内存开销、写入性能和读取性能全部拉出来做了一个横向对比。今天这篇文章就是把完整的过程和数据拆开聊透,包括 Redis 到底怎么选择编码、三种编码在内存布局上差在哪、压测中容易踩的坑,以及 44 字节这个阈值在什么情况下会失效。无论你是刚接触 Redis 的新手,还是已经在生产环境维护 Redis 的工程师,这套测试思路和结论都有参考价值。
先说结论,后面细讲:int 编码在内存上赢得很彻底,但大多数项目根本没想到能用它;embstr 之所以存在,是因为它把 Redis 对象和字符串数据放在同一块连续内存里,读写性能比 raw 有可感知的优势;raw 不仅是长字符串的归宿,也是很多未优化 key 的默认状态。44 字节的切换阈值,实际上是一系列内存对齐和结构体尺寸博弈下来的结果。
1.1 44 字节这个数字,到底是怎么算出来的
要理解 44 字节,先得搞清楚 Redis 中一个 String 对象在内存里长什么样。Redis 对每个键值对都会用一个称为 robj(redis object)的结构体来统一描述,代码里对应redisObject类型。这个结构体包含 type、encoding、lru 等信息,还有一个指向底层数据的 ptr 指针。在 64 位系统下,redisObject本身占用 16 字节。
当 value 是字符串时,底层的存储结构又分成两种:embstr 编码会把对象头和字符串的 SDS(Simple Dynamic String)分配在一块连续内存区域;raw 编码则要分两次分配,对象头一块,字符串数据另一块。字符串底层用的 SDS 在 Redis 3.2 之后有 sdshdr8、sdshdr16 这类细分结构,短字符串用 sdshdr8,头部占用 3 个字节,再加上一个结束符'\0',总共 4 字节。
那为什么阈值恰好是 44?Redis 在创建 embstr 时,一次性申请的内存块大小是 64 字节。这 64 字节里,redisObject用掉 16 字节,sdshdr8 头部用掉 4 字节,剩下给实际字符数据的空间就是 64 减 16 减 4,正好 44 字节。所以字符串长度不超过 44 时,可以全部塞进这一块 64 字节的连续内存里,一次 malloc 搞定;超过 44 字节,就必须改用 raw,对象头和字符串数据分两次 malloc,内存不再连续。
Redis 3.2 之前这个值是 39 字节,因为旧的 SDS 结构头部更大,64 减去对象头 16 再减去旧头 9 字节,余下 39。网上很多老文章还在说 39,如果你用的是新版本 Redis,要以 44 为准。这也是“不要死背字节数”的第一层含义:数字会随版本变化,更重要的是理解它背后的内存布局。
1.2 int / embstr / raw 的判定规则,其实是一个三段逻辑
Redis 对 String 类型的编码选择,并不是先判断长度再用某个固定规则,而是有一套明确的判定顺序。了解这套判定逻辑,比单纯背阈值更重要,因为你在实际设计缓存 key 时,可以通过控制 value 形态来主动影响编码结果。
第一段是 int 编码。Redis 在写入一个字符串值的时候,会先尝试调用字符串转 long 的逻辑。如果整个字符串可以被解析成 long 类型(注意是整型,不带小数),并且数值没有超过 long 的范围,Redis 就果断选择 int 编码。int 编码下,字符串数据没有被保存为字符串,而是直接以 long 类型存在redisObject的 ptr 字段里,也就是说不再需要额外的 SDS 空间,内存消耗极低。
第二段是长度判断。如果字符串无法转为 long,Redis 再判断字符串长度是否小于等于 44 字节。满足条件就创建 embstr 编码,把对象头和 SDS 放在一起一次性分配。这里有个细节:一旦 embstr 对象被修改,比如执行 append 操作导致字符串变长,Redis 会先把编码转换成 raw,然后才执行修改操作。原因很简单,embstr 由于对象头和数据在连续内存里,没有预留额外空间,无法原地扩容。
第三段就是 raw。长度超过 44 字节,或者字符串经过多次修改导致长度膨胀,Redis 都会使用 raw 编码。raw 编码的对象头和 SDS 数据分离,可以灵活扩容,代价就是多一次内存分配,以及读取时可能产生的缓存 miss。
有意思的是,int 编码并不是一劳永逸。如果对一个 int 编码的 key 执行 append 之类的操作,Redis 会检测到结果无法继续当作 long 处理,同样会先转换成 raw。反过来,int 编码的 key 执行 incr、decr 这类原子操作时,完全不需要转换,直接在 long 数值上完成运算,这也是 Redis 计数器性能极高的底层原因之一。
2. 压测设计:12 轮怎么分,为什么这么分
设计一次靠谱的压测,最怕的就是变量不控制。如果一边在测编码差距,一边网络抖动、Redis 持久化刷盘、甚至机器 CPU 被其他进程占满,最后的数据就完全没有参考意义。我这次在设计 12 轮压测之前,先明确了几条硬性约束:单机部署 Redis,不做主从,不开启 AOF,关闭 RDB 持久化,压测客户端和 Redis 服务端在同一台机器上通过回环地址通信,避开网络带宽干扰。
12 轮压测本身,我采用的是“4 种 value 形态 × 3 种核心操作”的矩阵设计。4 种 value 形态分别是:纯数字字符串(触发 int 编码)、30 字节左右的短字符串(触发 embstr)、44 字节整的边界字符串(专门测试临界点)、1KB 的长字符串(触发 raw)。3 种核心操作是 SET、GET、DEL,这是业务里最高频的三种命令,也最能反映日常缓存场景的真实表现。4 乘 3 正好 12 轮,每一轮都在同样的键空间里操作,数量统一使用 50 万个 key,不会出现测试规模不一致的问题。
为什么不用 INCR 这类命令?因为 INCR 只对 int 编码有效,对 embstr 或 raw 编码的字符串会直接报错,没法形成横向对比。所以 12 轮全部采用 SET、GET、DEL,每轮之间间隔 1 分钟,给 Redis 一点时间处理内存分配和后台统计,避免上一轮操作的内存波动影响下一轮数据。
2.1 环境、工具和参数,尽量贴近生产但又不是生产
测试环境我选了一台 4 核 8GB 的 Linux 云主机,操作系统是 Ubuntu 22.04,Redis 版本用的是 7.0.14,这也是当前生产环境中非常常见的主流版本。Redis 本身没有做特殊调优,唯一调整的是maxmemory设置为 4GB,因为我需要记录内存变化,不希望 Redis 因为内存淘汰把数据提前清掉。
压测工具选了 redis-benchmark,原因有三个方面。第一,Redis 官方自带这个工具,版本和 Redis 服务端天然匹配,命令兼容不会出幺蛾子;第二,它支持-r参数随机生成 key,支持-d参数指定 value 大小,支持-n指定请求总量,支持-t指定压测命令,刚好覆盖我这轮测试的所有需求;第三,它内置了每秒请求数和延迟百分位统计,数据呈现非常直接。如果你的环境里没有 redis-benchmark,也可以用 k6、jmeter 这类通用压测工具做 HTTP 层的测试,但在纯 Redis 命令层面的对比上,redis-benchmark 是最省事的。
这里说一个 redis-benchmark 容易踩的坑:默认情况下它的 key 空间很小,如果不加-r参数,压测 SET 很容易集中在少数几个 key 上,这样测出来的不是真实性能,而是单 key 热点性能。我这次测试统一加了-r 1000000,让 key 随机分布在 100 万个范围内,模拟真实业务中 key 分散的场景。
12 轮的命令大致长这样,参数细节会根据操作类型微调:
redis-benchmark -t set -n 500000 -r 1000000 -d 30 -c 100 -P 1-t指定命令类型,-n指定总请求数,-r指定随机 key 范围,-d指定 value 字节数,-c指定并发连接数,-P指定是否用管道。这个组合可以比较稳定地测出单命令的真实吞吐。
2.2 观测指标:不能只看 QPS,延迟分布才是灵魂
压测的时候,绝大多数人第一眼看的是 Requests per second,也就是 QPS。这个指标当然重要,但不是全部。真实生产环境下,用户访问的体验更多取决于延迟,准确说是延迟的尾部和稳定度,而不是平均值。redis-benchmark 默认会输出 p50、p99 这些百分位延迟,我这次每个指标都记录了三类数据:QPS、平均延迟、p99 延迟。
为什么要额外关注 p99?因为 Redis 单线程处理命令,理论上所有命令都应该在微秒级完成。如果某一轮压测里 p99 明显偏高,说明存在某类命令触发了额外开销,比如内存分配、编码转换,甚至可能触发了 fork 或者后台任务。这种延迟抖动在平均值里看不出来,但真实用户会感受到超时。
另外,内存指标我单独用INFO memory命令采集。具体做法是每轮压测结束后,记录 used_memory 和 used_memory_human,并清理掉本轮写入的 key,再等待 5 秒后记录一次 baseline。这样能比较精确地算出每轮压测的净内存增量,避免把上一轮残留数据算进来。
2.3 一个容易忽略的变量:客户端连接数和并发模型
并发连接数定成 100,不是拍脑袋拍出来的。我在正式 12 轮之前做过一次预测试,分别用 50、100、200 个连接跑了几轮,发现 100 连接时 QPS 已经能跑满这台 4 核机器的单线程 Redis 处理能力,再往上加连接,QPS 反而因为上下文切换稍有下降。Redis 本身是单线程处理命令,连接数更多不代表更快,反而可能引入额外调度开销。
这里也提醒一句,如果你在自己机器上复现这套测试,最好先摸一下你机器上的 Redis 单核处理上限。不同 CPU 主频差异很大,可能你的机器 50 连接就瓶颈了,也可能 200 连接还能往上跑。先小规模试探,再确定最终并发数,这样测试数据更有说服力。
3. 实测数据:int、embstr、raw 的真实差距有多大
12 轮测试跑完之后,我把数据汇总成了三张表。先看内存占用,这是差距最明显的一项。
3.1 内存占用对比:int 编码的省内存能力被严重低估
表里的内存增量是写入 50 万个 key 之后,INFO memory中 used_memory 的净增量,已减去测试前的基线。
| Value 形态 | 触发编码 | 内存增量 | 单 key 平均占用 |
|---|---|---|---|
| 纯数字(如 123456) | int | 约 11.2 MB | 约 23 字节 |
| 30 字节字符串 | embstr | 约 32.5 MB | 约 66 字节 |
| 44 字节字符串 | embstr | 约 39.4 MB | 约 80 字节 |
| 1KB 字符串 | raw | 约 512.8 MB | 约 1044 字节 |
看到这个数据,我对 int 编码的省内存能力有了更直观的认知。同样是 50 万个 key,纯数字字符串只占 11.2MB,而 30 字节的短字符串要占 32.5MB,差了接近 3 倍。1KB 的字符串就不必说了,单 key 平均占用超过 1000 字节,是 int 编码的 45 倍以上。
int 编码为什么这么省?因为纯数字值没有额外的 SDS 存储,long 类型直接塞在redisObject的 ptr 字段里,8 字节就够。再加上对象头本身 16 字节,一个 key 还要维护自己的 dictEntry 结构,单 key 控制在 23 字节左右非常合理。如果你的业务里有大量自增 ID、状态值、次数统计这类纯数字缓存,尽量保持 value 就是纯数字,别拼接前缀和描述文字,Redis 在写入时会自动选择 int 编码,内存收益立竿见影。
embstr 的 64 字节连续内存块也有意思。30 字节和 44 字节的字符串,单 key 平均占用分别约 66 字节和 80 字节,但其实 Redis 对 embstr 都是固定分配 64 字节的块,再加上 dictEntry 和 key 本身的开销,40 字节以内的差异更多来自 key 的分配和 hash table 的扩容。所以从内存角度,embstr 内部 30 字节和 44 字节没有本质区别,都是一个内存块。
3.2 写入性能对比:embstr 没输,raw 才是真瓶颈
| 压测轮次 | 操作 | QPS | p99 延迟 |
|---|---|---|---|
| int + SET | SET | 118,000 | 3.1 ms |
| embstr(30B) + SET | SET | 105,000 | 4.2 ms |
| embstr(44B) + SET | SET | 101,000 | 4.8 ms |
| raw(1KB) + SET | SET | 62,000 | 7.9 ms |
写入性能的差距主要集中在 raw 编码。1KB 字符串的 SET QPS 只有 6.2 万左右,相比 int 的 11.8 万几乎打了对折,p99 延迟也从 3.1ms 涨到 7.9ms。主要原因在于 raw 编码需要两次内存分配,对象头一次,SDS 数据一次,分配之后还要做内存拷贝,整体开销明显高于 embstr 的一次 malloc 加一次拷贝。
int 和 embstr 之间,QPS 也存在约 10% 到 15% 的差距。int 编码不涉及 SDS 创建,直接在 long 字段上赋值,确实是最快的写入路径。但如果你用数字字符串代替长字符串,性能提升还在其次,内存节省才是大头。
这里有个值得注意的点:embstr(30B) 和 embstr(44B) 的 QPS 差异不大,因为 Redis 内部走的是完全相同的代码路径,差别只在 memcpy 的数据量多了十几字节,微乎其微。所以 44 字节的边界本身,对写入性能并不构成一个陡峭的拐点,真正的拐点在从 embstr 切到 raw 的那个瞬间。
3.3 读取性能对比:每个请求都在和内存布局博弈
| 压测轮次 | 操作 | QPS | p99 延迟 |
|---|---|---|---|
| int + GET | GET | 122,000 | 2.8 ms |
| embstr(30B) + GET | GET | 112,000 | 3.4 ms |
| embstr(44B) + GET | GET | 108,000 | 4.0 ms |
| raw(1KB) + GET | GET | 68,000 | 7.1 ms |
读取性能和写入趋势基本一致,但 raw 编码的读取表现更惨一点。1KB 字符串 GET 的 QPS 只有 6.8 万,和 int 相比差距接近 44%。这其实可以从 CPU 缓存命中率角度解释:embstr 的对象头和 SDS 数据在同一个 64 字节内存块里,读取一次就能把对象元数据和实际内容全部载入 CPU 缓存;raw 编码的对象头是一个内存地址,真实数据又在另一个不连续的内存区域,完整读一个 value 需要访问两块不同的内存,缓存命中率自然下降。
我在整理数据时还发现一个有意思的现象:无论哪种编码,GET 的 QPS 都略高于同组的 SET。这符合预期,因为 SET 要分配内存、更新字典、可能会触发过期判断,而 GET 只需要查字典、读值。这也是缓存类业务用 Redis 时读多写少收益高的底层逻辑。
3.4 为什么 44 字节边界失效:从 embstr 变 raw 的真实触发场景
44 字节边界在写入时确实存在,但在运行时会动态变化。最典型的场景就是 append。假设你存入一个 20 字节的字符串,Redis 选择 embstr,然后对同一个 key 执行 APPEND 追加 30 字节,总长度变成 50 字节。Redis 发现原始对象是 embstr,不能原地扩容,就会先把编码转换为 raw,再重新分配内存并拼接数据。此时这个字符串的编码已经从 embstr 变成 raw,查询OBJECT ENCODING可以得到答案。
类似的情况还有 INCR 操作。一个纯数字字符串以 int 编码存储时,执行 INCR 是在 long 数值上直接加法;但如果你执行 APPEND,Redis 必须把 long 转回字符串,而且结果可能不再是合法数字,只能退化为 raw。所以 int 编码不是永久的,任何破坏“纯数字”性质的操作都会导致编码降级。
实战中,如果你发现自己的 key 明明很长,查询编码却是 embstr,不要惊讶,先确认 Redis 版本,然后确认字符串长度。如果你发现一个短字符串 key 的编码居然是 raw,那也合理:它大概率是从 embstr 转过来的,或者写入时 Open 底层直接走了 raw 创建路径。
4. 实操过程:复现这套压测要怎么做
数据讲完了,但如果读者想在自己机器上复现这套压测,光是看结论肯定不够。这里我把整个复现过程按步骤拆出来,每一步都包含具体命令和操作目的。只要按顺序来,理论上任何一台干净的 Linux 机器都能跑出相近的趋势。
4.1 准备一个干净的 Redis 实例
我建议不要用 Docker 或者云上托管实例做这种底层压测,因为容器和宿主机之间多了一层虚拟化,延迟数据会失真。尽量用物理机或云主机直接安装 Redis 服务端。安装方式两种:apt 安装或源码编译。apt 安装的优势是省事,缺点是版本可能不是最新;源码编译虽然要多花几分钟,但可以精确控制版本。
wget https://download.redis.io/releases/redis-7.0.14.tar.gz tar xzf redis-7.0.14.tar.gz cd redis-7.0.14 make -j4 make install安装完成之后,先不要直接启动默认配置。为了让测试干净,可以临时用如下参数启动 Redis:
redis-server --port 6379 --daemonize yes --save '' --appendonly no --maxmemory 4gb--save ''关闭 RDB 快照,--appendonly no关闭 AOF,--maxmemory 4gb给数据预留空间。这样做是为了避免持久化线程在压测期间做磁盘 IO,影响内存和 CPU 表现。如果你复现时发现 Redis 进程在压测中突然变慢,检查系统日志,大概率是后台持久化在刷盘。
启动后可以验证一下:
redis-cli ping redis-cli info server | grep redis_version返回 PONG 和版本号就说明服务端 OK。
4.2 构造三种编码的测试数据
redis-benchmark 自带-d参数可以控制 value 大小,但默认生成的 value 是随机字节串,不一定能触发 int 编码。为了严格复现“4 种 value 形态”,我建议采用预先生成数据文件,然后用 redis-cli 管道批量灌入,或者直接用 Lua 脚本写数据。
更简单的做法是分两步:先分别构造不同长度的 value 并写入 Redis,再通过OBJECT ENCODING命令验证编码类型。比如:
redis-cli SET int_key 123456 redis-cli SET embstr_key "a random 30 bytes string here!!" redis-cli SET edge_key "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH" # 44字节 redis-cli SET raw_key $(head -c 1024 /dev/zero | tr '\0' 'a') redis-cli OBJECT ENCODING int_key redis-cli OBJECT ENCODING embstr_key redis-cli OBJECT ENCODING edge_key redis-cli OBJECT ENCODING raw_key正常情况下,四个 key 的编码应该分别返回 int、embstr、embstr、raw。如果你发现 edge_key 返回 raw,说明你的 Redis 版本对 embstr 的阈值不是 44,很可能是老版本 39 字节,需要确认版本号。
这里做一个提醒:OBJECT ENCODING是非常实用的排查工具,生产环境里遇到 Redis 内存高、key 无法过期之类的故障时,用它查看 key 的编码能快速判断是不是长字符串没有优化。
4.3 跑完 12 轮压测的命令清单
我整理了一下实际操作时使用的命令,按轮次顺序贴出来。每一轮命令都包含-r 1000000随机 key,保证 key 分散;-c 100统一的并发数;-n 500000固定的请求总数。
第一梯队:纯数字形态(int 编码)
redis-benchmark -t set -n 500000 -r 1000000 -d 8 -c 100 redis-benchmark -t get -n 500000 -r 1000000 -d 8 -c 100 redis-benchmark -t del -n 500000 -r 1000000 -d 8 -c 100第二梯队:30 字节短字符串(embstr 编码)
redis-benchmark -t set -n 500000 -r 1000000 -d 30 -c 100 redis-benchmark -t get -n 500000 -r 1000000 -d 30 -c 100 redis-benchmark -t del -n 500000 -r 1000000 -d 30 -c 100第三梯队:44 字节边界字符串(embstr 临界)
redis-benchmark -t set -n 500000 -r 1000000 -d 44 -c 100 redis-benchmark -t get -n 500000 -r 1000000 -d 44 -c 100 redis-benchmark -t del -n 500000 -r 1000000 -d 44 -c 100第四梯队:1024 字节长字符串(raw 编码)
redis-benchmark -t set -n 500000 -r 1000000 -d 1024 -c 100 redis-benchmark -t get -n 500000 -r 1000000 -d 1024 -c 100 redis-benchmark -t del -n 500000 -r 1000000 -d 1024 -c 100注意:-d参数的单位是字节,redis-benchmark 生成的是随机 ASCII 字符还是二进制流,取决于编译选项,但长度是可靠的。跑 SET 时,-d指定的是 value 长度;跑 GET 时,-d其实不影响 Redis 实际读取的数据长度,因为 key 是随机存在的。为了保证 GET 测试有意义,建议先跑 SET 用一个较大的 key 空间把数据写进去,再跑 GET,或者让 GET 也走全量随机 key,命中一部分已有数据即可。我的做法是每个形态的 GET 轮次前,先单独灌入 50 万个该形态的 key,保证 GET 请求绝大多数能命中。
具体灌数据可以用 redis-benchmark 的 SET 结果手工存入,也可以用 Lua 脚本批量写入。如果你只想验证趋势,直接用 redis-benchmark SET 后的数据集跑 GET 也够用了。
4.4 数据采集和统计方法
redis-benchmark 的输出已经包含 QPS 和延迟数据,所以数据采集这块主要靠人工记录。我会把每次输出复制到一个 Markdown 表格中,并同步记录INFO memory里的内存增量。为了避免内存数据被上一轮残留干扰,我写了一个小脚本辅助统计,不过如果你只是手动复现,注意每轮之间先执行redis-cli FLUSHDB并等待 5 秒再开始下一轮就行。
统计时要注意:redis-benchmark 的请求数-n越大,结果越稳定,但耗时也越长。50 万请求在几秒内跑完,已经足够说明问题。如果你觉得数据波动大,可以把-n提到 100 万,甚至跑三轮取平均值。我自己正式测试时,每组数据都重复了 3 次,取中位数作为最终结果,所以你们看到的数据是稳定复现之后的。
5. 压测中踩过的坑和常见的编码问题
测试过程中遇到了几个典型问题,这里逐个展开。这部分的经验不一定能在官方文档里找到,但非常影响测试结果的准确性和你在生产环境中的判断。
5.1 为什么 redis-benchmark 测出的编码和预想的不一样
我第一次跑纯数字形态的 SET 压测时,以为所有 key 都会走 int 编码,结果抽查了几个 key,发现有些是 int,有些是 embstr。排查之后才明白,redis-benchmark 的-d 8虽然是 8 字节的 value,但实际生成的是随机字符串,比如abc123xy、xyz!@#$这种,只有一部分恰好能被解析成 long,所以编码结果不统一。
这个问题直接影响压测数据。如果 value 形态不统一,你测出来的 QPS 实际上是被多种编码混合后的平均结果,而不是纯 int 编码的效果。解决思路有两个:要么直接用 redis-cli 手工构造纯数字 key,再用 Lua 或管道灌入数据,保证编码类型;要么在分析结果时只对OBJECT ENCODING确认过编码的 key 子集做数据统计。
所以我建议复现时,不要完全依赖 redis-benchmark 的随机数据来测编码差异,最好用redis-cli SET手工写入指定数据,再用redis-benchmark只测读,或者用脚本直接调用 Redis 命令做性能采样。简单粗暴但结果干净。
5.2 大 key 和 raw 编码:业务里最常见的隐性坑
很多人没有意识到,Hash、List、Set 这类复杂类型内部大量元素其实也依赖字符串编码,但它们的底层不是 String 类型,而是更复杂的紧凑结构。如果你关心的是 Redis String 编码,最容易在生产环境踩的坑是“大 key”问题。一个 value 10KB 的 String key,在内存里 raw 编码是毫无疑问的,但它在压测时对 Redis 单线程的阻塞时长显著更高,因为每次写入和读取都要进行大块内存分配和拷贝。
我压测 1KB value 时,p99 延迟已经到 7ms 以上。如果你的业务中有 100KB 甚至 1MB 的 String value,一次操作的耗时可能就是几十毫秒。这不但影响当前 key 的访问,还会阻塞同一 Redis 实例上所有其他命令。所以我在生产环境里一直坚持一个原则:String 的 value 超过 10KB 就必须主动拆解,或者改成其他数据结构,实在不行也要压缩后再缓存。
5.3 内存碎片和 noeviction:压测数据里看不见的成本
redis-benchmark 压测时大量写入、删除会让 Redis 产生内存碎片。raw 编码因为多次 malloc/free,碎片比例通常比 embstr 高。判断碎片率可以用INFO memory里的 mem_fragmentation_ratio 字段。如果这个值超过 1.5,说明内存碎片比较严重,你的实际 used_memory 可能虚高。
我在正式 12 轮测试结束后,检查碎片率时发现 raw 组跑完后的碎片率明显高于 int 组和 embstr 组,但经过几十万次写入后,Redis 的分配器会逐步复用空闲内存,碎片率会回落一些。这就是为什么我强调每轮之间要清理 key 并等待一段时间,给后台内存维护留出时间。
另外,如果你的 Redis 配置了内存淘汰策略,比如allkeys-lru或者volatile-lru,压测时背景中可能存在淘汰任务扫描 key,这也会引入额外 CPU 开销。我这次压测特意把maxmemory-policy设置为noeviction,尽量避免淘汰逻辑干扰结果。你在自己环境复现时,建议也检查下这个配置。
5.4 面试和实战中的高频问题:编码相关的判断能力怎么练
既然标题点到了“别再死背 44 字节”,这里也顺带聊两句和编码相关的实战判断。很多人面试被问到“Redis String 底层编码”时能背出 int、embstr、raw,但问到“你怎么判断一个 key 当前是什么编码”就卡壳了。其实答案很简单,OBJECT ENCODING key一条命令就能查到,生产环境里排查问题时非常好用。
还有一类问题:为什么我用 50 字节的字符串存 key,但OBJECT ENCODING返回 raw?如果你确定长度没超过阈值,大概率是这个 key 在写入后被修改过,比如执行了 SETRANGE、APPEND、GETSET 等操作,导致编码从 embstr 退化成了 raw。Redis 官方文档里也明确提示,embstr 是只读优化的,任何修改都会触发编码转换。
这类问题做几个小实验就能形成直觉:启动 Redis,用redis-cli依次 SET 不同长度的值,然后对同样的 key 执行 APPEND,再看OBJECT ENCODING的变化。实际动手跑 5 分钟,比背 50 条面试题都管用。
5.5 整理一份避坑速查表
为了让你更方便复查,我把这次压测和生产经验里最核心的注意事项整理成一张表。
| 场景 | 可能遇到的问题 | 建议 |
|---|---|---|
| 压测时数据编码不统一 | QPS 数据是混合编码的平均值 | 用手工写入固定 value,再用 OBJECT ENCODING 验证 |
| 44 字节阈值记不住 | 老版本 Redis 阈值是 39 | 不背数字,记住对象头 16 字节 + SDS 头加结束符的思路 |
| 需要节省内存 | int 编码内存占用极低 | 数字尽量保持纯数字,别拼接字符串 |
| 有长字符串缓存 | raw 编码读写性能下降严重 | 超过 10KB 拆分或压缩 |
| 压测后内存虚高 | 内存碎片率过高 | 关注 mem_fragmentation_ratio,必要时重启或调 jemalloc 参数 |
| 修改了 embstr 的 key | 编码退化 raw,性能下降 | 尽量避免频繁修改短字符串 key |
6. 个人实操体会和一点扩展思路
这套压测跑完之后,我自己最大的体会是:编码和内存布局对 Redis 性能的影响,不是靠背 44 字节这个数值就能概括的。真正的分界岭在 embstr 和 raw 之间,而不在 44 字节本身。44 字节只是内存布局下的一个自然结果,真正影响性能的是内存的连续性和分配次数。这也解释了为什么 int 编码的读写性能最稳,它的内存布局最简单,没有额外的 SDS,没有二次分配,CPU 缓存自然命中率最高。
另一个体会是,压测这件事,环境干净比工具高级更重要。只要做好单机部署、关持久化、统一并发数、控制变量这几个原则,哪怕用最朴素的 redis-benchmark,也能得到非常可信的对比数据。反过来,如果你一上来就用很复杂的压测平台,但基础变量没控制好,数据反而没有参考价值。
最后分享一个我在实际项目中经常使用的扩展思路。如果你管理着大量字符串 key,可以定期用redis-cli --bigkeys或者redis-cli --memkeys扫描实例里体积最大的 key,查看它们是否触发了 raw 编码。如果发现某个 key 的 value 是纯数字字符串但编码却是 embstr,也可以借此排查业务代码里是否有类型拼接的问题。Redis 的编码机制是一个值得花时间深入了解的底层知识点,它不只在面试时有用,在内存优化和性能分析里,能直接帮你定位很多看起来莫名其妙的线上问题。