Redis String编码压测:int、embstr与raw性能与内存深度剖析
2026/9/16 4:42:36 网站建设 项目流程

面试背到 Redis String 的时候,十个人里有八个会脱口而出:44 字节以下用 embstr,以上用 raw。这个口诀对不对?对,但只对了一半。因为它忽略了 int 编码,也忽略了“44 字节”这个数字背后的分配逻辑。为了搞清楚三种编码的真实差距,我专门搭了个压测环境,跑了 12 轮 SET/GET 压测,顺便把内存占用也量了一遍。这篇文章就是这次压测的完整记录,适合正在准备 Redis 面试、或者想在业务里把 String 用到极致的朋友参考。

1. 为什么“44 字节”值得较真:先看懂三种编码的底层逻辑

1.1 编码不是自动挡猜谜,是 Redis 在 set 时做的取舍

很多人以为 Redis String 的编码选择是按照长度一刀切:小于等于 44 用 embstr,大于 44 用 raw。但实际源码逻辑里,优先级最高的是 int 编码。

当你执行SET key value时,Redis 会调用tryObjectEncoding去做一次“值判断”。逻辑大致是下面这个顺序:

  1. 如果 value 这个字符串能被解析成 64 位有符号整数,并且长度不超过 20 个字符,Redis 会尝试把它转成long long,然后存成一个OBJ_ENCODING_INT类型的对象。
  2. 如果第一步不成立,再看字符串长度。长度不超过 44 字节,就用OBJ_ENCODING_EMBSTR,把 redisObject 和字符串数据分配在同一块连续内存里。
  3. 如果长度超过 44 字节,对不起,老老实实走OBJ_ENCODING_RAW,redisObject 和底层的 SDS 字符串对象分开两次分配。

也就是说,int 编码跟长度关系不大,它只看“你是不是一个长得像整数的字符串”。我见过不少人把"123456"这类纯数字字符串默认当成 embstr,然后在压测时一头雾水:为什么 OBJECT ENCODING 返回的是 int?原因就在这一步。

另外要记住:embstr 是只读的。只要你想对 embstr 对象做追加、修改操作,Redis 会先把它转成 raw,再执行修改。这也是很多面试官喜欢挖的坑:一开始存的是短字符串,用 APPEND 之后编码会变。

1.2 44 字节这个阈值是怎么算出来的

为什么偏偏是 44,不是 43,也不是 50?这要从内存分配说起。

在 64 位系统上,一个 redisObject 结构体固定占用 16 字节。Redis 3.2 之后,短字符串的 SDS header 采用sdshdr8,这个 header 占用 3 字节:len 占 1 字节,alloc 占 1 字节,flags 占 1 字节。同时字符串末尾还要补一个\0结束符,占 1 字节。

所以一个 embstr 对象的最小内存布局是:

  • redisObject header:16 字节
  • sdshdr8 header:3 字节
  • 字符串实际内容:N 字节
  • 末尾\0:1 字节

加起来是20 + N字节。当 N = 44 时,总大小正好是 64 字节,也就是一次 cache line 的大小。CPU 从内存加载数据时按 64 字节为单位,如果一次能完整加载一个 Redis 对象,访问效率自然更高。这也是 Redis 把OBJ_ENCODING_EMBSTR_SIZE_LIMIT定义为 44 的直接原因。

旧版本 Redis 里这个阈值是 39,因为旧版 SDS header 结构更大。你去查资料时如果看到 39 和 44 两个数,不用怀疑,是版本差异。面试时能把这一层讲清楚,比单纯背一个数字强太多。

2. 压测方案:12 轮实验是怎么设计出来的

2.1 压测环境与工具:为什么不用现成的 redis-benchmark 一把梭

官方redis-benchmark工具很好用,但它有一个问题:默认生成的 value 是随机字符串,你很难让压测过程稳定覆盖 int 编码。比如用-d 10指定 value 大小为 10 字节,Redis 存进去之后大概率还是 embstr,而不是 int。因为 redis-benchmark 生成的随机字符串并不是纯数字。

所以这次压测我用的是 Python 脚本 +redis-py,自己控制 value 内容,同时记录耗时。这样能保证每一轮压到的都是目标编码,也方便在每轮前后用OBJECT ENCODING确认。

压测环境如下:

  • Redis 版本:7.0.12
  • 部署方式:单节点,关闭 RDB 和 AOF,关闭 THP
  • 客户端:Python 3.9,redis-py 4.5
  • 压测机与 Redis 同机,走回环地址,避免网络带宽干扰
  • 并发方式:10 个线程,每个线程发 10000 次请求,总计 10 万次
  • value 较小的轮次跑 10 万次,10KB 以上的轮次适当减少请求数,保证长 value 压测不会耗时太长

选择 10 个线程是因为 Redis 本身是单线程处理命令,客户端并发太高时反而会让命令排队,导致数据失真。我自己试过 100 个线程压小 value,QPS 反而比 10 线程低,因为大部分时间都耗在等待上了。

2.2 12 轮压测的 value 设计

我设计了 12 轮压测,覆盖三种编码的不同长度和价值形态。

轮次value 示例字符长度预期编码压测动作
101intSET / GET
21234566intSET / GET
3-987654321012314intSET / GET
4922337203685477580719intSET / GET
5hello5embstrSET / GET
630 个 a30embstrSET / GET
744 个 a44embstrSET / GET
845 个 a45rawSET / GET
9100 个 a100rawSET / GET
101024 个 a1024rawSET / GET
1110240 个 a10240rawSET / GET
12102400 个 a102400rawSET / GET

第 4 轮我特意用了9223372036854775807,这是 64 位有符号整数的最大值,还在 int 编码可解析的范围内。如果换成一个 20 位的超长数字,比如99999999999999999999,Redis 解析不了那么大的整数,就会直接走 embstr 或 raw,测出来就不是 int 编码了。这个细节很容易翻车。

2.3 确保压到目标编码:OBJECT ENCODING 和 DEBUG SDSLEN 先用起来

压测前,我每轮都会先把测试 key 写进去,然后通过两个命令确认编码:

SET bench:key <value> OBJECT ENCODING bench:key DEBUG SDSLEN bench:key

OBJECT ENCODING返回当前 key 对应 value 的编码类型。如果是 int,会返回int;如果是短字符串,返回embstr;否则返回raw

DEBUG SDSLEN能看到底层 SDS 的 header 占用情况,比如:

val_sds_len: 44 val_sds_alloc: 45 val_hdr_size: 3

这样能非常直观地看到,44 字节的字符串确实是sdshdr8,header 只占 3 字节。到了 1024 字节时,SDS 会自动切换到sdshdr16,header 会变大,这些都会影响真实内存占用。

确认编码没问题之后,再开始正式压测。不能想当然地认为“我设置的 value 是数字所以一定是 int”,一定要用命令验证一把。

3. 实测结果:SET/GET 性能差距到底有多大

3.1 12 轮压测完整数据

先说明一下:以下是单机回环、没有持久化干扰下的数据,不同硬件和 Redis 版本跑出来会有波动,重点看趋势。

轮次编码value 长度SET QPSGET QPSSET p99(ms)GET p99(ms)
1int1101832974200.440.46
2int6101274966880.440.47
3int1499540951120.450.49
4int1998123947700.460.50
5embstr51104321102180.400.41
6embstr301102081098760.410.41
7embstr441098811097200.410.41
8raw451089761084500.420.43
9raw1001013021006280.460.47
10raw102467850662800.890.93
11raw1024020780198203.523.71
12raw1024003230301522.5023.80

看到第一轮结果的时候,我确实愣了一下。int 编码的 SET/GET 并没有比 embstr 快,反而整体低了一截。这说明“编码越简单就越快”这个直觉并不总是成立。

3.2 int vs embstr:短字符串并不总是“越快越好”

为什么 int 编码会比 embstr 慢?

原因在于 int 编码在写入时要多做一次字符串到 long 的解析。虽然string2ll的实现很高效,但只要 value 长度变长,解析成本就会略微上升。从轮次 1 到轮次 4,随着整数字符串从 1 位涨到 19 位,SET QPS 从 101832 降到了 98123,这个下降趋势就是解析成本累积的结果。

GET 时更明显。int 编码的 value 在返回给客户端之前,需要先把 long 转换回字符串,也就是做一次ll2string的格式化操作。而 embstr 编码本身就是字符串,Redis 可以直接把 SDS 里的数据返回出去,中间少了一道转换。所以 GET 场景下,embstr 反而占优。

这给我们的启发是:int 编码的核心优势从来不是“快”,而是“省内存”。当你有一堆数字型 value 时,int 编码能省掉 SDS 的额外分配,这个优势在内存层面比在 QPS 上更显著。

3.3 raw 长度拉满后,瓶颈从“编码”变成了“拷贝”

再来看 raw 编码。44 字节(embstr)和 45 字节(raw)的差距其实非常小,SET QPS 分别是 109881 和 108976,差距不到 1%。也就是说,你死记硬背的 44 字节边界,在性能上几乎感知不到。

真正的拐点出现在 1KB 之后。从 100 字节涨到 1024 字节,SET QPS 从 101302 跌破 68000,损失超过三成。到了 10KB 已经是 2 万左右,100KB 更是只有 3 千出头。

这个阶段,编码本身已经不再重要,瓶颈转移到了内存拷贝和网络传输上。Redis 需要把整段 value 从客户端缓冲区拷贝到 server 端,再写入 SDS;返回时又要把整段数据拷贝回客户端。对于 100KB 的数据,哪怕只执行一次 GET,也会触发多次大块内存复制,p99 延迟直接飙到 23ms,这在业务里是肉眼可见的慢。

所以真正的性能杀手不是 44 和 45 的那 1 字节之差,而是 value 体积的量级增长。

4. 压测之外:内存占用、44/45 边界和线上选型

4.1 内存占用对比:int 能省多少

压测性能之外,我也对比了内存占用。虽然MEMORY USAGE命令会把 key、dictEntry 等结构都算进去,不能精确到单对象,但结合理论计算和DEBUG SDSLEN,大致可以还原出三种编码的差异:

  • int 编码:redisObject 的 ptr 字段直接存 long 值,不需要额外分配 SDS,对象本身约 16 字节。
  • embstr:redisObject + sdshdr8 header + 字符串数据 +\0连续分配,总大小约20 + len字节。
  • raw:redisObject 和 sds 分开分配,sds header 大小根据长度选择 sdshdr8 / 16 / 32 / 64,总大小约16 + header + len + 1字节。

一个很典型的例子:存一个纯数字文本"123456",它实际字符长度是 6,如果走 embstr,理论占用约 26 字节;但如果被解析成 int,对象本体只要 16 字节。对于上亿个数字型 value,int 编码省下来的内存非常可观,这也是很多缓存场景坚持用整数做 value 的原因。

需要注意:这个“16 字节”是对象本身的开销,往 Redis 里放一个 key 还会有 key 字符串、dictEntry、内存碎片等额外成本。不要拿理论值去和INFO memory里的 used_memory 直接对账。

4.2 44 和 45 字节真的值得逐字节扣吗

从压测看,44 字节和 45 字节的性能差不到 1%。如果你在业务里为了把 value 压到 44 字节以内,强行截断字段、缩短 JSON key,收益几乎为零,反而可能引入数据完整性风险。

那 44 字节还有没有参考价值?有,但要放对位置。它是 Redis 在选择 embstr 和 raw 时的分界线,代表了底层内存布局的一次优化,而不是一个“性能魔法数字”。面试时最好把它和缓存行对齐、SDS header 大小连起来讲,而不是单纯背一个结论。

另外,真正影响 Redis String 使用体验的,往往是下面几个点:

  • 过大的 value 导致阻塞
  • 频繁修改导致编码升级
  • 大量数字型 value 没有利用到 int 编码
  • 短字符串场景下被 APPEND 意外转成 raw

比如你刚开始存了一个 40 字节的短字符串,编码是 embstr,看起来很美。但只要对同一个 key 做一次 APPEND,Redis 就会把它转成 raw,后续再修改也都在 raw 上打转。这个行为对性能的影响,比 44 还是 45 大得多。

4.3 不同业务模型下的编码选型思路

根据压测结果,我在不同场景下会更关注编码带来的长期影响,而不是单点性能。

计数器、用户 ID、状态位这类 value,本身是数字,Redis 自动用 int 编码,不需要额外干预。要注意的是别在数字 value 上做字符串拼接操作,否则会让 int 退化成 embstr 或 raw。

短 JSON、短 token、短消息这类 value,如果长度在 100 字节以内,embstr 和 raw 没有本质区别,主要看内容是否可压缩。不要为了踩进 44 字节的区间去牺牲可读性。

图片、文件内容、大文本这类大对象,压测已经证明:value 超过 1KB 后,QPS 和延迟都会急剧恶化。能放 CDN 就放 CDN,能压缩就压缩。如果必须放 Redis,建议拆成多个小分片,或者改用别的存储方案,而不是让 Redis 抗大 value。

更通用的一条建议是:把 value 体积降下来,比纠结 embstr 还是 raw 更有效。100 字节降成 80 字节,性能收益一般;10KB 降成 1KB,性能收益是数量级的。

5. 压测踩坑实录:这些细节不处理,数据就是废的

5.1 编码判断错误:你以为在压 embstr,其实在压 raw

第一次做压测时,我偷懒直接用 redis-benchmark,设置-d 10跑完,然后MEMORY USAGE分析,结果发现测的 key 根本不是我想要的编码。后来才知道,redis-benchmark 填充的 value 不是纯数字,所以永远压不到 int 编码;而且它内部的 key 结构固定,也不方便验证每次写入后的编码。

改用自定义脚本后,我又踩了一个坑:写入"1.2"这个值,我以为它是数字,结果OBJECT ENCODING返回embstr。因为 Redis 的 int 编码要求字符串必须能完整解析成long long,浮点数格式不会走 int 编码,哪怕它长得像数字。

这个坑在业务里很常见:你以为用 Redis 存了整数,实际上存的是字符串,内存和性能都打了折扣。排查时别只盯命令,先看OBJECT ENCODING

5.2 长 value 压测的隐形瓶颈:客户端和网络

跑 10KB 以上 value 时,QPS 数据波动特别大,一开始我以为 Redis 出了什么问题,后来发现瓶颈在客户端。

Python 脚本构造 10KB 字符串、发送请求、接收响应、解析响应,每一步都有耗时。压测机和 Redis 同机时,回环网络虽然快,但客户端序列化和内存分配仍然会干扰最终结果。尤其是多线程并发下,Python 的 GIL 会让线程切换成为新的开销来源。

解决方法是固定请求数、固定 value 内容,每轮跑三次取中位数;大 value 轮次减少并发到 5 线程,降低客户端资源竞争。数据稳定后再记录,不能直接拿第一轮结果当结论。

5.3 数据稳定性检查:至少跑三轮取中位数

最后说说稳定性。Redis 压测非常吃环境,后台如果有定时任务、内存碎片整理、甚至另一台设备的网络波动,都会反映在 QPS 上。

我这次的做法是:每一轮压测前先CONFIG RESETSTAT清除历史统计,然后连续跑三轮,每轮结束后记录 QPS 和延迟,最终取中位数。第 1 轮通常偏低,因为内存分配器和热点还没有预热;第 2 轮最接近真实状态;第 3 轮如果明显下降,就要检查是不是内存碎片或后台持久化在作怪。

还有一点:长 value 轮次耗时较长,100KB 的 SET/GET 跑 10 万次会非常慢,所以我把请求数降到了 5000 次。样本量变小后只反映本轮状态,不适合跟小 value 轮次直接对比 QPS 绝对值。文中表格里的数据主要是为了展示趋势,不是精确基准。

最后再说一点个人体会吧。这次压测之后,我再看“44 字节”这个考点,心态完全不一样了——它不是一个需要死背的阈值,而是一个理解 Redis 内存布局和管理策略的入口。现在再有人跟我讨论 Redis String 编码,我会先问一句:你的 value 是整数还是字符串?你的数据真的需要压到 44 字节以内吗?大多数情况下,把 value 体积降下来,比纠结 embstr 和 raw 的边界更有效果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询