土法炼钢兴趣小组的算法知识备份

【Redis / 缓存内核】紧凑编码:listpack · quicklist · intset

文章导航

分类入口
databasestorage
标签入口
#redis#redis-kernel#listpack#quicklist#intset#ziplist#compact-encoding#memory

目录

Dict 与渐进 rehash 解决「键值索引怎么扩」;当集合本身很小,再用 dict + 指针森林往往比数据还贵。Redis 长期策略是:小集合用连续内存紧凑编码,超限再升级为 hashtable / quicklist / skiplist。7.4 主线里,ziplist 已从运行态移除(OBJ_ENCODING_ZIPLIST 仅作历史常量),由 listpack 接替;List 小对象可直接 LISTPACK,大对象用 quicklist(双向链表 + listpack 节点)。

本文钉 Redis OSS 7.4.2src/listpack.csrc/quicklist.csrc/intset.ct_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.cLP_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_EXserver.h):在 listpack 旁挂扩展元数据(expire_time 等),超过紧凑阈值仍落到 OBJ_ENCODING_HT。这说明「紧凑编码」不是冻结的——新语义可能先落在 HT + 旁路索引 上,而非继续压 listpack。

RDB 边界:加载旧版 RDB 时 rdb.c 仍识别 ziplist 类型码并转换为 listpack / quicklist(ziplistlistpack 迁移函数),保证 磁盘格式向后兼容内存格式单一主线 分离。在线写入路径不再生成 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

配置 list-max-listpack-size(别名 list-max-ziplist-size)默认 -2config.c):语义为每个 listpack 节点目标约 8KB 量级,负值表示按字节上限而非固定元素个数。


三、intset:有序整数数组

Set 在元素可解析为整数且规模小时用 intsetOBJ_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 或超条目上限则转 listpackhashtablesetTypeCreate 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;插入使节点超限则 listpackConvertToQuicklistt_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 与规范化的整数编码降低变长元数据脆性。争论在于:

与 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 级(源码)

B 级

站内交叉


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

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .