Redis 紧凑编码:listpack、quicklist 与 intset

原文发布于 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;
flowchart LR ql[&#34;quicklist&#34;] n1[&#34;node: listpack<br/>count=small&#34;] n2[&#34;node: listpack&#34;] n3[&#34;node: LZF compressed&#34;] ql --> n1 --> n2 --> n3
  • 创建 :createQuicklistObject(fill, compress) → OBJ_ENCODING_QUICKLIST(object.c)。
  • 小 list 反向降级 :仅当 quicklist 只有一个 PACKED 节点 且长度/字节低于 list-max-listpack-size 推导的阈值,可转回 OBJ_ENCODING_LISTPACK(t_list.c listTypeConvertQuicklistToListpack)------避免频繁插入删除导致抖动。
  • 压缩 :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)O(n) 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 有窄条件降级

七、三句话小结

  1. listpack 接替 ziplist,作为 hash/set/zset/list 的小块连续编码;quicklist 把大 list 切成 listpack 节点链表,可选 LZF。
  2. intset 服务小整数 set;超 set-max-intset-entries 或出现非整数则升级。
  3. 升级阈值由 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 级

站内交叉


返回 系列目录 | 上一篇:Dict 与渐进 rehash | 下一篇:ZSet 与 Stream

相关推荐
IT大白鼠5 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz5 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙5 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除5 天前
redis--018
redis
两锭子5 天前
Redis基础
redis
烟沙九洲5 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索5 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞5 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路6 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog6 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis