编码切换的阈值
前面四页讲的紧凑编码有一个共同前提:元素不多。元素多起来之后,顺序扫描与整块 memmove 都会变成瓶颈,得换成 哈希表 或 skiplist 那样的索引结构。
Redis 把这个切换做成了自动的。每个值对象带一个 encoding 字段,写入时检查几条阈值,越界就当场转换。本页把判据与真实出厂值列清楚。
1 · 类型与编码的对照
OBJECT ENCODING 报的是内部编码而非类型。同一个类型可能报出几个不同的值:
| 类型 | 紧凑编码 | 索引编码 |
|---|---|---|
| string | int、embstr |
raw |
| list | listpack |
quicklist |
| set | intset、listpack |
hashtable |
| hash | listpack |
hashtable |
| zset | listpack |
skiplist |
Redis 7.0 之后 ziplist 这个返回值不再出现,全部换成 listpack;RDB 里的旧格式在加载时就地转换。
string 那一行的判据与元素个数无关。能解析成 long 的走 int,直接把整数存进 robj 的指针字段;长度不超过 44 的走 embstr,robj 与 SDS 一次分配在同一块 64 字节内存里;再长走 raw,两次分配。44 这个数的来历见
SDS:Redis 的字符串 §2。
2 · 集合类的判据
其余四种类型的判据都是「个数」与「单个元素长度」两条,任一越界即转换,且此后不再检查。
具体到配置项与 Redis 7.4 的出厂值:
| 类型 | 个数阈值 | 长度阈值 |
|---|---|---|
| hash | hash-max-listpack-entries 512 |
hash-max-listpack-value 64 |
| zset | zset-max-listpack-entries 128 |
zset-max-listpack-value 64 |
| set(非整数) | set-max-listpack-entries 128 |
set-max-listpack-value 64 |
| set(全整数) | set-max-intset-entries 512 |
不适用 |
| list | list-max-listpack-size -2 |
不适用 |
警示 · 最后两行与流传较广的说法不一致,值得核对源码而不是凭印象。config.c 里 hash-max-listpack-entries 的出厂值是 512,128 是 Redis 6 及更早 hash-max-ziplist-entries 的值;list-max-listpack-size 的出厂值是
-2,负数表示按字节封顶,-2 对应 8 KB,与元素个数无关。本页这两个数是先按 128 写进正文,查 config.c 之后改的。
list 的这条按字节算,临界点随元素长度浮动。item-0 到 item-828 这 829 个短字符串编成 8187 字节的 listpack,仍在 8192 以内;再加一个变成 8197 字节,越界,转成 quicklist。同样的 8 KB 换成一批 100 字节的元素,八十个就到顶了。
set 那两行之间有一处不连贯。全整数的 set 走 intset,上限 512;越过 512 之后并不会退到 listpack,因为 set-max-listpack-entries 只有 128,根本装不下。t_set.c 的 maybeConvertIntset 也确实是直接调 setTypeConvert(subject, OBJ_ENCODING_HT)。一个 513 个整数的 set
就这样从每元素 2 字节的 intset 一步跨到 hashtable,中间那档最省内存的过渡完全跳过。
3 · 单向切换与 list 的例外
hash、set、zset 转成索引编码之后不会转回来。把元素删到只剩一个,OBJECT ENCODING 报的仍是 hashtable 或 skiplist。
理由与缩容那道题同源,见 扩容的摊还与增长因子 §4:往回转要重新扫一遍全部元素判断阈值,而且判据一旦对称,在阈值附近增删就会反复转换。省掉这条路,代价是使用方得知道「峰值决定了这个 key 此后的内存形态」。
list 是唯一的例外,从 Redis 7.2 起支持转回来。listTypeTryConvertQuicklist 的条件有三条:整个 quicklist 只剩一个 packed 节点、节点字节数与元素数都不超过上限,而且在「因删除而缩小」这条路径上,两个上限都要先除以二。
if (shrinking) {
sz_limit /= 2;
count_limit /= 2;
}
除以二这一步就是滞后区间。出厂的 -2 下,涨到 8192 字节才转 quicklist,而跌回 4096 字节以下才转回 listpack,中间那 4 KB 什么都不做。函数上方的注释把动机写明了:to avoid frequent conversions of quicklist and listpack due to frequent insertion and deletion。
同一道题在本系列出现了三次,每次的答案都是同一个形状:动态数组的缩容阈值要比扩容阈值低得多,Redis dict 的缩容看
而扩容看
,list 的回转看上限的一半。判据在阈值处对称,操作序列就能在那里榨出无限多次重分配。
4 · 参考文献
- Redis. config.c.
redis/src/config.c(Redis 7.4):standardConfig表里各*-max-listpack-*的出厂值。 - Redis. t_set.c 的
maybeConvertIntset与 t_list.c 的listTypeTryConvertQuicklist(Redis 7.4)。 - Redis. OBJECT ENCODING 命令手册与 Memory optimization 文档。
- Sanfilippo, S. (2011). Redis 内存优化笔记:紧凑编码阈值的量级依据与调参建议。