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.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):

text 复制代码
[ 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 单一大元素):

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_QUICKLISTobject.c)。
  • 小 list 反向降级 :仅当 quicklist 只有一个 PACKED 节点 且长度/字节低于 list-max-listpack-size 推导的阈值,可转回 OBJ_ENCODING_LISTPACKt_list.c listTypeConvertQuicklistToListpack)------避免频繁插入删除导致抖动。
  • 压缩list-compress-depth 对距头尾一定深度外的节点做 LZF(quicklistLZF),省内存换 CPU。

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


三、intset:有序整数数组

Set 在元素可解析为整数且规模小时用 intsetOBJ_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 或超条目上限则转 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):

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;插入使节点超限则 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 与规范化的整数编码降低变长元数据脆性。争论在于:

  • 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.2src/listpack.csrc/listpack.h
  • Redis OSS 7.4.2src/quicklist.csrc/quicklist.h
  • Redis OSS 7.4.2src/intset.csrc/intset.h
  • Redis OSS 7.4.2src/t_hash.csrc/t_set.csrc/t_list.csrc/config.c(默认值)

B 级

站内交叉


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

相关推荐
野生技术架构师13 小时前
Redis 和 MySQL 如何保证数据一致性?先更新数据库还是先删缓存,延迟双删、MQ、Canal 一次讲透
数据库·redis·缓存
Devin~Y13 小时前
从内容社区到AI智能客服:Spring Boot + Spring Cloud + Spring AI 全栈实战面试拆解
java·spring boot·redis·elasticsearch·spring cloud·kafka·mybatis
2602_9599609214 小时前
电商大厂Java面试:从Spring Boot、JPA、微服务到Redis、Kafka、Spring Security与监控,谢飞机的爆笑三轮问答
java·jvm·spring boot·redis·面试题
愈努力俞幸运14 小时前
token,context window,top_p,temperature,skill,Harness Engineering
数据库·redis·缓存
谷哥的小弟1 天前
腾讯云服务器安装Redis Stack图文教程
服务器·redis·腾讯云·redis stack
NeilYuen1 天前
【vemory】高性能KV存储AOF方案设计
linux·c++·redis·缓存
spter。1 天前
Redis 与 Single-flight 的分工与协作
数据库·redis·缓存
小林ixn2 天前
用 Next.js 和 Redis 撸一个 Markdown 笔记系统:RSC 实战与组件化拆解
前端·redis·next.js
北漂燕郊杨哥2 天前
Win11 环境下 phpStudy 安装 Redis 最新版,并为 PHP 8.5.9(NTS)配置 redis 扩展
数据库·redis·php