news 2026/9/16 20:54:41

Redis String编码探秘:44字节边界与int/embstr/raw实测对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis String编码探秘:44字节边界与int/embstr/raw实测对比

先说结论: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 位有符号整数(比如-92233720368547758089223372036854775807)时,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,不会再回到 int;embstr 只要被修改一次,也立刻转成 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 里,总占用必须不超过 64:

16 字节 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.x,SDS 结构有过调整,sdshdr8 依旧存在,短字符串阈值逻辑没变。但如果你自己编译 Redis 时换了 tcmalloc 或者 libc malloc,size 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-benchmark:Redis 官方自带的基准工具,简单直接,适合快速看 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 万 QPS,GET 大约 16 万 QPS,INCR 大约 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(host='127.0.0.1', port=6379, decode_responses=True) # 构造指定长度的纯 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_usageobject encoding会告诉你真正的编码方式和内存占用,这两个命令是做这类对比时最该用的工具。

压测部分我用了一个线程池开 50 个连接,每轮跑 10 万次操作,统计总耗时:

from concurrent.futures import ThreadPoolExecutor import time import redis def bench_set(length, total=100000, threads=50): pool = redis.ConnectionPool(host='127.0.0.1', port=6379, max_connections=threads) all_keys = [f'bench:{length}:{i}' for i in range(total)] def worker(keys): r = redis.Redis(connection_pool=pool) for k in keys: r.set(k, 'a' * length) return len(keys) chunk = total // threads tasks = [all_keys[i*chunk:(i+1)*chunk] for i in range(threads)] start = time.time() with ThreadPoolExecutor(max_workers=threads) as ex: list(ex.map(worker, tasks)) cost = time.time() - start print(f'length={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 字节 'a'embstrSET0.71140k临界点前
543 字节 'a'embstrGET0.64156k临界点前
643 字节 'a'embstr → rawAPPEND0.87114kappend 后编码转 raw
744 字节 'a'embstrSET0.73136k临界点最大值
844 字节 'a'embstrGET0.65152k临界点最大值
945 字节 'a'rawSET0.79126k临界点刚过
1045 字节 'a'rawGET0.67148k临界点刚过
11100 字节 'a'rawSET0.84119k更长字符串
12100 字节 'a'rawAPPEND0.92108k修改长字符串

看完原始数据,有几点要立刻说明。

第一,int 编码在写入和读取上确实都是最快的,但优势是 5%~8% 左右,不是“秒杀”。因为 Redis 本身命令处理、网络回环、客户端解析这些固定开销占了很大比例。

第二,44 字节附近的性能差距真实存在,但没有“断层”。43 字节 SET 是 140k,44 字节是 136k,45 字节是 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 太小(常见默认 8),JMeter 里 100 个线程并发时全在等连接,测出来的 QPS 完全是连接池的瓶颈,跟 Redis 编码关系不大。我建议至少配到maxTotal=200maxIdle=100

第二个是 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 字节,不如把内存预算算清楚,那才是真正影响成本的地方。

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

去水印批量下载:douyin-downloader 从配好到跑通的实操笔记

去水印批量下载:douyin-downloader 从配好到跑通的实操笔记 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback …

作者头像 李华
网站建设 2026/9/16 20:52:04

SQL报错注入实战详解:CTFHUB场景下UpdateXML、ExtractValue与Floor三种手法

CTF圈的兄弟应该都对SQL注入不陌生,在CTFHUB技能树里,报错注入算是基础里很有代表性的一类题型。它不像联合查询那样需要看着回显位置拼字段数,也不像盲注那样一个个字符去猜,而是在页面报错信息里直接“看到”数据库吐出来的数据…

作者头像 李华
网站建设 2026/9/16 20:51:52

V4L2与高通KMD深度剖析:Camera驱动架构与调试实战

做Camera驱动的兄弟应该都有同感:不管你是做手机平台的BSP,还是做车载、安防的Camera方案,只要SoC选了高通平台,几乎绕不开两个名字——V4L2和高通KMD。V4L2是Linux内核里视频设备驱动的标准框架,高通KMD则是高通Camer…

作者头像 李华
网站建设 2026/9/16 20:51:04

OptiScaler 指南:老显卡也能换超分技术

OptiScaler 指南:老显卡也能换超分技术 【免费下载链接】OptiScaler OptiScaler bridges upscaling/frame gen across GPUs. Supports DLSS2/XeSS/FSR2 inputs, replaces native upscalers, enables FSR-FG/XeFG on non-FG titles. Supports Nukem mod for DLSSG-to-FSR3 FG. …

作者头像 李华
网站建设 2026/9/16 20:49:46

Ubuntu黑屏卡tty1?显示管理器故障诊断与修复指南

1. 这不是系统崩溃,而是图形显示服务“掉线”了——先搞清本质再动手Ubuntu启动后卡在黑底白字的tty1界面,很多人第一反应是“系统坏了”“装错了”“驱动炸了”,急着重装系统。其实绝大多数情况下,这根本不是系统损坏&#xff0c…

作者头像 李华