一、redisObject 结构
1.1 一切皆对象
Redis 中的每个 key 和 value 都被包装成一个 redisObject,它承载类型、编码和引用计数,是内存管理的基础:
// 简化后的 redisObject 结构(Redis 6+ 用位域压缩)
typedef struct redisObject {
unsigned type:4; // 数据类型: string/list/hash/set/zset
unsigned encoding:4; // 底层编码: int/embstr/listpack/...
unsigned lru:LRU_BITS; // 记录访问时间,用于淘汰
int refcount; // 引用计数,0 时释放
void *ptr; // 指向底层数据结构
} robj;
1.2 逻辑类型与底层编码分离
关键设计:用户看到的类型(type)与内存存储方式(encoding)是分离的。同一个 Hash 类型,数据少时用紧凑的 listpack,数据多了自动转为 hashtable:
# 逻辑类型相同,底层编码不同
TYPE myhash # hash
OBJECT ENCODING myhash # listpack(小数据)→ hashtable(大数据)
1.3 为什么需要多编码
| 编码 | 内存 | 访问速度 | 适用 |
|---|---|---|---|
| 紧凑编码(int/listpack) | 省 | 略慢(需解码) | 小数据 |
| 哈希表/跳表 | 费 | 快 | 大数据 |
核心思路:小数据用紧凑编码换内存,大数据用散列结构换速度。Redis 在编码转型阈值内自动切换,无需人工干预。
二、String 编码选择
2.1 三种编码
String 有三种编码:int(整数)、embstr(短字符串内联)、raw(长字符串):
SET n 100
OBJECT ENCODING n # int 64 位内可用整数
SET s hello
OBJECT ENCODING s # embstr <= 44 字节短字符串
SET t "$(python3 -c 'print("x"*100)')"
OBJECT ENCODING t # raw 超过 44 字节
2.2 编码切换规则
# int → raw:整数追加字符后编码被破坏
APPEND n abc
OBJECT ENCODING n # raw
# embstr 只读不可变:修改后转 raw
SET s hello
APPEND s world
OBJECT ENCODING s # raw
| 编码 | 条件 | 特点 |
|---|---|---|
| int | 值在 64 位整数范围 | 直接存数值,零字符串开销 |
| embstr | 长度 ≤ 44 字节 | 对象与字符串内存连续分配 |
| raw | 长度 > 44 字节 | 两次分配,可修改 |
embstr 的 44 字节来自「redisObject 头 + SDS 头 + 结尾符」在一页内存内的紧凑布局。短字符串首选 embstr,能显著降低分配次数与碎片。
三、List 编码(quicklist/listpack)
3.1 演进历史
Redis List 的底层编码经历了 ziplist → quicklist 的演进,Redis 7.0 又引入 listpack 替换 ziplist:
# 创建 List 并观察编码
RPUSH mylist a b c d
OBJECT ENCODING mylist # listpack(Redis 7+)
| 版本 | 底层结构 | 说明 |
|---|---|---|
| <3.2 | ziplist / linkedlist | 小用 ziplist,大用双向链表 |
| 3.2~6.x | quicklist | 分片 ziplist 串成链表 |
| 7.0+ | quicklist(节点内 listpack) | ziplist 彻底退役 |
3.2 quicklist 结构
quicklist 是「压缩块 + 链表」的折中:每个节点是一个紧凑的 listpack,多个节点用双向链表连接:
list-max-listpack-size 128 # 单个 listpack 节点最大条数
list-compress-depth 0 # 两端压缩深度,0=不压缩
# 大量元素时
RPUSH biglist $(seq 1 100000)
OBJECT ENCODING biglist # quicklist
quicklist 的设计目的是:既保留 listpack 的紧凑内存,又避免单个 listpack 过大导致插入 O(n)。列表过长时拆成多个节点,插入在节点边界处很便宜。
四、Hash 编码(listpack/hashtable)
4.1 小 Hash 用 listpack
Hash 字段数少、值小时用 listpack 连续存储,字段与值交替排列:
HSET h f1 v1 f2 v2
OBJECT ENCODING h # listpack
# 观察 listpack 布局(紧凑排列)
# |len|f1|v1|f2|v2|...|end|
4.2 转型阈值
# 超过任一阈值即转 hashtable
hash-max-listpack-entries 128 # 字段数阈值
hash-max-listpack-value 64 # 单字段值字节数阈值
# 突破阈值后
for i in $(seq 1 200); do HSET bighash f$i v$i; done
OBJECT ENCODING bighash # hashtable
4.3 选择权衡
| 编码 | 内存 | 读写性能 | 适用 |
|---|---|---|---|
| listpack | 省 60%+ | 字段少时够快 | 小 Hash(对象、配置) |
| hashtable | 费 | O(1) 稳定 | 大 Hash |
Hash 是最典型的「小对象优化」受益者:用户画像、会话等天然小字段对象用 listpack 存储,内存可省一半以上。字段超过阈值才会升级,生产上多数 Hash 都停留在 listpack。
五、Set 编码(intset/hashtable)
5.1 intset 整数集合
当 Set 全为整数且数量少时,用 intset(有序整数数组,二分查找):
SADD myset 1 2 3 4 5
OBJECT ENCODING myset # intset
# 插入大整数或字符串会破坏 intset
SADD myset 99999999999999999999
OBJECT ENCODING myset # hashtable
5.2 转型条件
set-max-intset-entries 512 # 超过 512 个整数则转 hashtable
# intset 特性:全整数有序、二分查找 O(log N)、内存定长紧凑
# 插入非整数/超范围数 → 立即升级
5.3 对比
| 编码 | 成员要求 | 查找复杂度 | 内存 |
|---|---|---|---|
| intset | 全整数 | O(log N) | 省 |
| hashtable | 任意 | O(1) | 费 |
整型 ID 集合(如「已购买商品」)用 intset 存储非常省内存。但删除导致数量下降不会降级——编码只会从小升大,不会反向。
六、ZSet 编码(skiplist/listpack)
6.1 双结构设计
ZSet 的经典实现是 skiplist(跳表)+ dict(字典) 的组合:跳表按 score 排序,字典按 member 定位:
ZADD z 1 a 2 b 3 c
OBJECT ENCODING z # listpack(小数据,Redis 7+)
# 大数据时
# object encoding: skiplist
# skiplist 结构
# dict: member -> score (快速定位分数)
# skiplist: score 排序链表 (范围查询)
6.2 转型阈值
zset-max-listpack-entries 128 # 成员数阈值
zset-max-listpack-value 64 # member 长度阈值
# 小 ZSet 用 listpack 紧凑存储(member+score 交替)
ZADD bigz $(for i in $(seq 1 500); do echo $i member$i; done)
OBJECT ENCODING bigz # skiplist
6.3 选择权衡
| 编码 | 适用 | 典型操作 |
|---|---|---|
| listpack | ≤128 成员 | 排行榜小集合 |
| skiplist | 大集合 | ZRANGEBYSCORE 分页、Top-N |
排行榜、延迟队列这类 ZSet,数据规模小时用 listpack 省内存;规模上来后自动转 skiplist,保证
ZRANGE、ZRANGEBYSCORE的高效。阈值可调,但默认值已是经验平衡点。
七、编码转型阈值
7.1 阈值总览
| 类型 | 配置项 | 默认值 | 转型方向 |
|---|---|---|---|
| Hash | hash-max-listpack-entries | 128 | listpack→hashtable |
| Hash | hash-max-listpack-value | 64 | listpack→hashtable |
| ZSet | zset-max-listpack-entries | 128 | listpack→skiplist |
| ZSet | zset-max-listpack-value | 64 | listpack→skiplist |
| Set | set-max-intset-entries | 512 | intset→hashtable |
| List | list-max-listpack-size | 128 | listpack 节点拆分 |
7.2 转型不可逆
# 编码升级是单向的:小升大之后不会因数据减少而降级
SADD s 1 2 3
OBJECT ENCODING s # intset
SADD s a # 转 hashtable
SREM s a
OBJECT ENCODING s # 仍是 hashtable,不回 intset
单向转型意味着:峰值数据量大时编码会永久停留在高位。如果预期集合偶尔很大,但长期很小,调高阈值能让多数时间停留在紧凑编码。
7.3 调整阈值的影响
| 调整 | 收益 | 风险 |
|---|---|---|
| 调高 entries 阈值 | 更多小对象用紧凑编码 | 单次 O(N) 操作变慢 |
| 调高 value 阈值 | 更长字符串进紧凑编码 | 大 value 拷贝开销 |
| 调低阈值 | 更快升级 | 内存上升 |
阈值调整前用
OBJECT ENCODING统计现有对象的编码分布,再结合内存与 CPU 权衡。默认值对绝大多数业务已是最优,盲目调大可能让写操作变慢。
八、OBJECT ENCODING 观察
8.1 常用命令
# 对象信息命令
OBJECT ENCODING key # 底层编码
OBJECT REFCOUNT key # 引用计数
OBJECT IDLETIME key # 空闲秒数(配合 LRU)
OBJECT FREQ key # 访问频率(LFU)
# 批量观察
redis-cli --scan --pattern "user:*" | \
while read k; do echo -n "$k "; redis-cli object encoding "$k"; done
8.2 诊断内存分布
# 用 redis-cli --bigkeys 找出大 key 与编码分布
redis-cli --bigkeys
# 输出各类型最大 key、其编码、元素数量
| 诊断项 | 关注点 |
|---|---|
| 大 key 的编码 | 是否已升级为散列结构 |
| 小 key 却用大编码 | 曾达到转型阈值后未降级 |
| 元素总量 | 判断是否超阈值 |
8.3 编码观察实践
# 生产上定期抽检
# 1. 统计各类型编码分布
# 2. 定位异常(如 3 字段的 hash 竟用 hashtable)
# 3. 结合内存优化手段处理
OBJECT ENCODING是内存优化的「听诊器」。发现「本应紧凑却用散列」的对象,通常是历史峰值导致的不可逆升级,可用重写(如HGETALL后重建)恢复紧凑编码。
九、内存优化策略
9.1 紧凑存储
| 策略 | 做法 | 效果 |
|---|---|---|
| 小对象聚合 | 字段合并进一个 Hash | 省大量 key 元数据 |
| 整数复用 | 用整数代替字符串 | 触发 int 编码 |
| 短 key 名 | 控制 key 长度 | 减少元数据 |
| 大 key 拆分 | 拆分超大集合 | 避免单结构膨胀 |
# 共享整数:0~9999 的整数对象全局共享
SET a 100
OBJECT REFCOUNT a # 引用计数很高(共享)
# 大整数/字符串则不共享
9.2 过期与清理
# 惰性删除(访问时查)+ 定期删除(每 100ms 抽样)+ 主动碎片整理
CONFIG SET activedefrag yes
# 删除大 key 时避免阻塞主线程
UNLINK bigkey # 异步释放内存
9.3 优化实战
# 案例:1 亿条 user:xxx:field 散列字符串 → 聚合为 Hash
# 优化前:1 亿个 key,元数据约 100MB+
# 优化后:2500 万 Hash,每个 4 字段,内存降 40%+
| 手段 | 内存收益 | 注意 |
|---|---|---|
| listpack 编码 | 小对象省 50%+ | 阈值别盲目调大 |
| int 编码 | 数值省 90% | 需整数语义 |
| 共享整数 | 0~9999 免重复分配 | 只对 int 生效 |
| 过期清理 | 释放失效数据 | 惰性+定期配合 |
| 碎片整理 | 回收 jemalloc 碎片 | CPU 开销 |
内存优化三原则:先看 OBJECT ENCODING 诊断,再选紧凑结构,最后配合过期与清理。不要为了「看着省」破坏读写性能,一切以压测数据为准。
结语
- redisObject 将逻辑类型与底层编码分离,同一个逻辑类型可对应多种内存编码,这是内存优化的基础。
- String 有 int/embstr/raw 三种编码,44 字节以内短字符串用 embstr,整数用 int 零字符串开销。
- List 用 quicklist(内含 listpack 节点)折中内存与插入性能,ziplist 在 Redis 7.0 后退役。
- Hash 小对象用 listpack,超过 entries/value 阈值转 hashtable,是「小对象优化」的最大受益者。
- Set 全整数且数量少时用 intset 二分查找,插入非整数或超阈值即升级为 hashtable。
- ZSet 小数据用 listpack,大数据用 skiplist+dict 组合,支撑 O(log N) 的范围查询。
- 编码转型单向不可逆,峰值数据量大时会永久停留高位,可用重写恢复紧凑编码。
- 用 OBJECT ENCODING 诊断编码分布,配合紧凑存储、共享整数、过期清理与碎片整理实施内存优化。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。