算法与数据结构 / 动态数组与紧凑编码 · 从摊还扩容到 listpack / 编码切换的阈值 待审核 6 / 6
OBJECT ENCODING · 出厂值 · 单向切换

编码切换的阈值

前面四页讲的紧凑编码有一个共同前提:元素不多。元素多起来之后,顺序扫描与整块 memmove 都会变成瓶颈,得换成 哈希表 或 skiplist 那样的索引结构。

Redis 把这个切换做成了自动的。每个值对象带一个 encoding 字段,写入时检查几条阈值,越界就当场转换。本页把判据与真实出厂值列清楚。

1 · 类型与编码的对照

OBJECT ENCODING 报的是内部编码而非类型。同一个类型可能报出几个不同的值:

类型 紧凑编码 索引编码
string intembstr raw
list listpack quicklist
set intsetlistpack hashtable
hash listpack hashtable
zset listpack skiplist

Redis 7.0 之后 ziplist 这个返回值不再出现,全部换成 listpack;RDB 里的旧格式在加载时就地转换。

string 那一行的判据与元素个数无关。能解析成 long 的走 int,直接把整数存进 robj 的指针字段;长度不超过 44 的走 embstrrobj 与 SDS 一次分配在同一块 64 字节内存里;再长走 raw,两次分配。44 这个数的来历见 SDS:Redis 的字符串 §2。

2 · 集合类的判据

其余四种类型的判据都是「个数」与「单个元素长度」两条,任一越界即转换,且此后不再检查。

图 2-1 · 给定元素个数与元素长度时,四种集合类型各自会落到哪种编码,以及是哪一条阈值定的。可改元素个数、元素长度与是否全为整数。

具体到配置项与 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.chash-max-listpack-entries 的出厂值是 512,128 是 Redis 6 及更早 hash-max-ziplist-entries 的值;list-max-listpack-size 的出厂值是 -2,负数表示按字节封顶,-2 对应 8 KB,与元素个数无关。本页这两个数是先按 128 写进正文,查 config.c 之后改的。

list 的这条按字节算,临界点随元素长度浮动。item-0item-828 这 829 个短字符串编成 8187 字节的 listpack,仍在 8192 以内;再加一个变成 8197 字节,越界,转成 quicklist。同样的 8 KB 换成一批 100 字节的元素,八十个就到顶了。

set 那两行之间有一处不连贯。全整数的 set 走 intset,上限 512;越过 512 之后并不会退到 listpack,因为 set-max-listpack-entries 只有 128,根本装不下。t_set.cmaybeConvertIntset 也确实是直接调 setTypeConvert(subject, OBJ_ENCODING_HT)。一个 513 个整数的 set 就这样从每元素 2 字节的 intset 一步跨到 hashtable,中间那档最省内存的过渡完全跳过。

3 · 单向切换与 list 的例外

hash、set、zset 转成索引编码之后不会转回来。把元素删到只剩一个,OBJECT ENCODING 报的仍是 hashtableskiplist

理由与缩容那道题同源,见 扩容的摊还与增长因子 §4:往回转要重新扫一遍全部元素判断阈值,而且判据一旦对称,在阈值附近增删就会反复转换。省掉这条路,代价是使用方得知道「峰值决定了这个 key 此后的内存形态」。

图 3-1 · 先把元素灌过阈值再删回去,逐步显示四种类型的编码与内存量级。可切换类型与峰值元素数,观察哪一种会在删回去之后恢复紧凑编码。

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 的缩容看 α<0.1\alpha < 0.1 而扩容看 α1\alpha \ge 1,list 的回转看上限的一半。判据在阈值处对称,操作序列就能在那里榨出无限多次重分配。

4 · 参考文献

  1. Redis. config.c. redis/src/config.c(Redis 7.4):standardConfig 表里各 *-max-listpack-* 的出厂值。
  2. Redis. t_set.cmaybeConvertIntsett_list.clistTypeTryConvertQuicklist(Redis 7.4)。
  3. Redis. OBJECT ENCODING 命令手册与 Memory optimization 文档。
  4. Sanfilippo, S. (2011). Redis 内存优化笔记:紧凑编码阈值的量级依据与调参建议。