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):
[ 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 单一大元素):
// 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;flowchart LR
ql["quicklist"]
n1["node: listpack<br/>count=small"]
n2["node: listpack"]
n3["node: LZF compressed"]
ql --> n1 --> n2 --> n3
- 创建:
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):
// 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):
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 规范:https://github.com/antirez/listpack(格式说明)
- Redis 官方:Memory optimization
站内交叉
返回 系列目录 | 上一篇:Dict 与渐进 rehash | 下一篇:ZSet 与 Stream
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Redis / 缓存内核】对象模型与 SDS:redisObject · type · encoding
拆解 Redis 7.4/8.x 的 redisObject 多态层与 SDS 二进制安全字符串:type/encoding/refcount 如何分工、字符串四种编码的升级路径、OBJECT ENCODING 能读到什么,以及紧凑表示与统一抽象之间的工程争论。
【Redis / 缓存内核】ZSet 与 Stream:skiplist · rax · 消费组
Redis 7.4/8.x 有序集合的双结构 skiplist+dict 与小 listpack 编码、Pugh 跳表参数如何落地,以及 Stream 的 rax+listpack 存储与 consumer group / PEL 的最小必要语义——不扩写消息队列教科书。
【Redis / 缓存内核】缓存服务器全景:HashMap · Memcached · Redis · 嵌入 KV
定位进程内 HashMap、Memcached、Redis 与 RocksDB 嵌入 KV 在内存缓存栈中的生态位;划分与 architecture/17、storage/66、linux/redis-source 的分工,并给出本系列 16 篇阅读地图。
【Redis / 缓存内核】事件循环与 I/O:ae · 客户端缓冲 · IO threads
按 Redis OSS 7.4/8.x 源码拆解 aeEventLoop 文件/时间事件、beforeSleep 钩子与客户端 querybuf/reply 路径,并精确说明 IO threads 只卸载读写与解析、命令仍在主线程执行。