原文发布于 quant67.com,转载请保留出处。
Dict 与渐进 rehash 解决「键值索引怎么扩」;当集合本身很小,再用 dict + 指针森林往往比数据还贵。Redis 长期策略是:小集合用连续内存紧凑编码,超限再升级为 hashtable / quicklist / skiplist 。7.4 主线里,ziplist 已从运行态移除(OBJ_ENCODING_ZIPLIST 仅作历史常量),由 listpack 接替;List 小对象可直接 LISTPACK,大对象用 quicklist(双向链表 + listpack 节点)。
本文钉 Redis OSS 7.4.2 的 src/listpack.c、src/quicklist.c、src/intset.c 及 t_hash.c / t_set.c / t_list.c 中的升级触发,不伪造 OBJECT ENCODING 输出。
一、listpack:ziplist 的继任与布局
历史边界 :ziplist 是单块连续内存,存 entry 链;中间插入/删除需 级联更新 (cascade update)后面 entry 的 prevlen,大 entry 时可能 O(n) 内存搬移,且曾有越界类漏洞面。listpack 2017 年起独立规范(listpack.c 头部指向 antirez/listpack 规格),Redis 6/7 逐步把 hash/set/zset/list 的小编码迁到 listpack。
listpack 逻辑布局(LP_HDR_SIZE = 6):
text
[ total bytes (32b) | num elements (16b) | entry | entry | ... | 0xFF EOF ]
每个 entry 自带 backlen 编码,支持从尾部 O(1) 定位前一 entry;整数有多种变长编码(7bit/13bit/16bit...,listpack.c 宏 LP_ENCODING_*)。与 ziplist 不同,listpack 设计上 避免 prevlen 级联 这一脆弱点(维护者文档与 issue 讨论是 B 级线索;A 级以当前 listpack.c 实现为准)。
Hash 的小编码:OBJ_ENCODING_LISTPACK,field/value 成对紧凑存放;Set 小编码也用 listpack 存 member 串;ZSet 小编码用 member + score 交替(zzl* 辅助函数在 t_zset.c)。
7.4 还为带字段级 TTL 的 Hash 引入 OBJ_ENCODING_LISTPACK_EX(server.h):在 listpack 旁挂扩展元数据(expire_time 等),超过紧凑阈值仍落到 OBJ_ENCODING_HT。这说明「紧凑编码」不是冻结的------新语义可能先落在 HT + 旁路索引 上,而非继续压 listpack。
RDB 边界 :加载旧版 RDB 时 rdb.c 仍识别 ziplist 类型码并转换为 listpack / quicklist(ziplist → listpack 迁移函数),保证 磁盘格式向后兼容 与 内存格式单一主线 分离。在线写入路径不再生成 ziplist。
二、quicklist:listpack 的分片链表
单块 listpack 过大时,中间插入仍要搬移整块内存。List 的默认大编码 quicklist 把数据切成多个 quicklistNode,每节点一个 listpack(或 PLAIN 单一大元素):
c
// src/quicklist.h --- quicklistNode 32 字节
typedef struct quicklistNode {
struct quicklistNode *prev, *next;
unsigned char *entry; /* listpack 或 LZF 压缩块 */
size_t sz;
unsigned int count : 16;
unsigned int encoding : 2; /* RAW / LZF */
unsigned int container : 2; /* PLAIN / PACKED */
...
} quicklistNode;
- 创建 :
createQuicklistObject(fill, compress)→OBJ_ENCODING_QUICKLIST(object.c)。 - 小 list 反向降级 :仅当 quicklist 只有一个 PACKED 节点 且长度/字节低于
list-max-listpack-size推导的阈值,可转回OBJ_ENCODING_LISTPACK(t_list.clistTypeConvertQuicklistToListpack)------避免频繁插入删除导致抖动。 - 压缩 :
list-compress-depth对距头尾一定深度外的节点做 LZF(quicklistLZF),省内存换 CPU。
配置 list-max-listpack-size(别名 list-max-ziplist-size)默认 -2 (config.c):语义为每个 listpack 节点目标约 8KB 量级,负值表示按字节上限而非固定元素个数。
三、intset:有序整数数组
Set 在元素可解析为整数且规模小时用 intset (OBJ_ENCODING_INTSET):
c
// src/intset.c --- 编码宽度随数值升级
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX) return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX) return INTSET_ENC_INT32;
else return INTSET_ENC_INT16;
}
intset 是有序数组,查找/插入用二分 + memmove;当新元素超出当前 encoding 宽度时 整表升级 (16→32→64 bit)。非整数 member 或超条目上限则转 listpack 或 hashtable(setTypeCreate in t_set.c)。
四、encoding 升级触发与默认阈值
升级是 单向 为主(list 有特殊降级)。下表为 config.c 默认值 (均可 CONFIG SET):
| 配置项 | 默认 | 影响 |
|---|---|---|
hash-max-listpack-entries |
512 | Hash 字段对数超则 → hashtable |
hash-max-listpack-value |
64 字节 | 单 field/value 超则 → hashtable |
set-max-intset-entries |
512 | intset 超限 → hashtable |
set-max-listpack-entries |
128 | listpack set 超限 → hashtable |
set-max-listpack-value |
64 | 单 member 超则升级 |
zset-max-listpack-entries |
128 | ZSet 条目超则 → skiplist+dict |
zset-max-listpack-value |
64 | member 长度超则升级 |
list-max-listpack-size |
-2 | quicklist 节点 listpack 目标大小 |
Hash 示例 (t_hash.c):hashTypeConvert 在字段数或单值长度超限时分配 dict 并把 listpack 条目迁入;HFE(字段级 TTL)等 7.4 新特性在 hashtable 路径上扩展(第 7 篇不展开)。
Set 工厂 (setTypeCreate):
c
robj *setTypeCreate(sds value, size_t size_hint) {
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK
&& size_hint <= server.set_max_intset_entries)
return createIntsetObject();
if (size_hint <= server.set_max_listpack_entries)
return createSetListpackObject();
robj *o = createSetObject();
dictExpand(o->ptr, size_hint);
return o;
}
List :空列表常建 listpack;插入使节点超限则 listpackConvertToQuicklist(t_list.c)。
本地可用 OBJECT ENCODING 观察迁移(需自行运行 redis-server;本站本环境未部署,不粘贴输出)。
粗算内存口径 :紧凑对象的真实 RSS 还受 jemalloc bin、键名 SDS、以及 redisObject 头部影响。MEMORY USAGE key 对 listpack 会 Walk 内部条目(默认采样子元素),适合对比「同逻辑数据在 listpack vs hashtable」的数量级,不宜与 INFO memory 总量直接相减当「节省倍数」。
五、学术谱系与争论
紧凑数组编码 在存储引擎中可追溯至「行存/列存边界上的小数组优化」------与论文级「列式压缩」(如 Lightweight Compression 一类)不同,Redis 的 listpack 是 单值 CPU 解码、无 SIMD 扫描 的嵌入式格式。
ziplist → listpack 不是学术会议论文,而是 工程事故驱动的重构:级联更新使最坏情况 CPU 与安全隐患并存;listpack 用 backlen 与规范化的整数编码降低变长元数据脆性。争论在于:
- A 派(省内存) :社交时间线、Session 类 workload 大量小 hash/set,紧凑编码可显著降 RSS(具体倍数依赖对象形状,无统一 benchmark 可引为站内结论)。
- B 派(省 CPU / 可预测延迟) :热点 key 一旦升级失败或反复跨越阈值,转换本身是一次 O(n) 拷贝;更大争议是 不会自动缩回 ------删到只剩 1 个字段仍占着
hashtable桶数组。
与 B-Tree / 列存对比 :紧凑 listpack 不适合范围扫描外的复杂查询;Redis 的定位是 O(1) 字段访问,不是 analytics。开放问题是:模块类型与 JSON 是否应绕过 listpack 直接上大结构(第 16 篇选型一句)。
六、常见误解
| 误解 | 事实 |
|---|---|
| 7.4 仍用 ziplist 服务在线路径 | 运行态小编码是 listpack;ziplist 常量仅兼容旧 RDB |
调大 *-max-listpack-* 只省内存 |
更大紧凑块 → 中间插入搬移成本升,单 key 延迟尖刺 |
| intset 可存任意字符串 | 仅 可解析为整数 的 member;否则 listpack/ht |
| quicklist 等于一个 listpack | quicklist 是 多节点;单节点才可能降级为纯 listpack |
| 删元素后会自动变回 listpack | 多数 type 不会自动降级;list 有窄条件降级 |
七、三句话小结
- listpack 接替 ziplist,作为 hash/set/zset/list 的小块连续编码;quicklist 把大 list 切成 listpack 节点链表,可选 LZF。
- intset 服务小整数 set;超
set-max-intset-entries或出现非整数则升级。 - 升级阈值由
hash/set/zset/list-max-*配置 钉死,默认偏「小对象省内存」;跨越阈值是一次性结构替换,通常不可逆。
参考资料
A 级(源码)
- Redis OSS 7.4.2 :
src/listpack.c、src/listpack.h - Redis OSS 7.4.2 :
src/quicklist.c、src/quicklist.h - Redis OSS 7.4.2 :
src/intset.c、src/intset.h - Redis OSS 7.4.2 :
src/t_hash.c、src/t_set.c、src/t_list.c、src/config.c(默认值)
B 级
- listpack 规范:github.com/antirez/lis...(格式说明)
- Redis 官方:Memory optimization
站内交叉
返回 系列目录 | 上一篇:Dict 与渐进 rehash | 下一篇:ZSet 与 Stream