Theory · Redis · Internals
redisObject 壳 + SDS / dict / skiplist / listpack / quicklist 芯 —— type 定行为,encoding 定实现,阈值定切换
五大类型只是"壳":redisObject 用 type/encoding 双字段解耦逻辑类型与底层实现,OBJECT ENCODING 直接可查
SDS O(1) 长度、dict 渐进式 rehash、skiplist 概率平衡、listpack 消灭连锁更新——全部对照 redis 源码讲
紧凑编码 ↔ 常规编码的阈值配置(7.x vs 8.0 变化)、只升不降原则,是面试的追问主战场
Why Encodings Matter
// 存用户画像:一个用户一个 hash,每个 5 个字段 > HSET u:1 name alice age 30 city bj vip 1 lv 7 > OBJECT ENCODING u:1 "listpack" // 紧凑编码:整块连续内存 > MEMORY USAGE u:1 120 // 约 120 字节,每字段 ≈24B // 换一个大 hash:塞进 600 个字段(超过默认阈值 512) > OBJECT ENCODING u:big "hashtable" // 自动换成常规哈希表 > MEMORY USAGE u:big // 每字段 ≈100B 起:多出 dictEntry(24B) // + 两个 sds 头 + 桶数组 + 空槽预留 // 而对业务代码来说,HGET / HSET 的用法、 // 返回值、复杂度承诺,全都没变过一个字。
假设 hash 永远是标准哈希表:存 5 个字段也要为每对键值分配一个 dictEntry(键指针 + 值指针 + next 指针 ≈24B)、两个独立的字符串对象头,再加上桶数组必须留空槽(负载因子要 <1)。元数据比数据本身还大——小对象场景,"通用"等于"浪费"。
每个 key 的值都套一个统一外壳 redisObject,壳上写着两件事:我是什么类型(type,给用户看)和我现在用哪种底层实现(encoding,内部优化)。数据小的时候用省内存的紧凑实现,大到一定程度自动换成快的常规实现,而上层命令完全不知情。
① 它直接是钱:编码选择决定内存账单,是缓存集群最大的成本项;
② 它解释怪现象:"删了一半数据内存却不降"、"同样 10 万条数据这个 key 特别大",答案都在编码;
③ 它是面试的深水区:从"五大类型"追问到 sdshdr8、rehashidx、连锁更新,报得出结构体名字和常量的人一眼可辨。
先认识壳(redisObject)与类型×编码的全局地图 → 再逐个拆五种芯(SDS / dict / skiplist / listpack / quicklist·intset)→ 最后收到"什么时候换芯"的阈值表、速查页与 16 道 QA。
Prerequisites & Glossary
| 术语 | 一句话理解(细节后面展开) |
|---|---|
| 对象 / robj (redisObject) | 每个 key 的值都被套上的统一外壳,记着类型、编码、访问时钟等元信息 |
| type / encoding | type = 用户看到的类型(string/hash/…);encoding = 内部真正用的数据结构。同一个 type 可以有多种 encoding |
| 紧凑编码 | 把所有元素塞进一整块连续内存的实现(listpack、intset):省内存、但改动要挪字节,只适合小数据 |
| SDS | Redis 自己写的字符串(Simple Dynamic String):字节数组 + 一个记录长度的头部 |
| dict / 桶 / 负载因子 | 哈希表;桶(bucket)是数组的一格,冲突的键挂成链表;负载因子 = 元素数 ÷ 桶数,越大冲突越多 |
| rehash | 桶数组换大(或换小)后,把所有键按新桶数重新分配位置的过程 |
| 跳表(skiplist) | 多层链表:上层稀疏用来"跨大步"、底层完整且有序,查找像跳台阶,期望 O(log N) |
| quicklist | list 的实现:一条双向链表,但每个节点内部塞一块紧凑内存(而不是只放一个元素) |
| 写时复制(COW) | fork 出子进程后父子共享内存页,谁写谁才复制一份——父进程改得越多,额外内存越多 |
| jemalloc 分配类 | 内存分配器只按固定档位给内存(16/32/64/128B…):申请 45B 实际拿到 64B,多出来的是浪费 |
哈希表 → 桶、冲突、链地址法、负载因子的通用原理
跳表 → 层高、概率平衡、与平衡树的完整对比
数组 vs 链表 → "连续内存 vs 指针跳转"的取舍(紧凑编码的理论基础)
往后看:过期与淘汰 →(robj 里 lru:24 字段的用途)、缓存问题 →(大 key 治理)
结构体名与常量一律用源码里的原文(sdshdr8、rehashidx、ZSKIPLIST_P),因为面试报得出名字才算真读过;配置项一律用 7.0 之后的新名(*-max-listpack-*),旧名只在讲版本变迁时出现。
一个 key = 一个壳(robj)+ 一个芯(底层结构)。
· 壳上写着"我是什么类型"和"我现在用哪种芯";
· 芯有两类:紧凑芯(连续内存、省钱、慢改)与常规芯(哈希表/跳表、费内存、快);
· 数据小用紧凑芯,超过阈值自动换常规芯,而且只换一次、永不换回。
后面 11 页只回答两个问题:有哪些芯(结构原理)、什么时候换芯(阈值与转换规则)。
Object System · src/server.h
开场那两条 OBJECT ENCODING 的输出差异,就写在这个壳的两个 4bit 字段里:type 决定命令能不能用,encoding 决定这份数据实际长什么样。
// github.com/redis/redis · src/server.h // Redis 7.x / 8.x · robj(redisObject) typedef struct redisObject { unsigned type:4; // 逻辑类型 unsigned encoding:4; // 底层编码 unsigned lru:LRU_BITS; // 24bit:LRU 时钟或 LFU int refcount; // 引用计数 / 内存回收 void *ptr; // 指向底层结构 } robj; // type:OBJ_STRING / OBJ_LIST / OBJ_SET // OBJ_ZSET / OBJ_HASH(4bit 够用) // encoding:同 type 的不同底层实现 // LRU_BITS = 24(第 16 页展开 LFU 拆分)
| 字段 | 要点 |
|---|---|
| type:4 | 面向用户的五大类型:STRING / LIST / SET / ZSET / HASH;由命令名决定校验(LPUSH 只认 LIST) |
| encoding:4 | 同一 type 的多种底层实现,如 hash 的 listpack / hashtable——行为对外一致,内存与复杂度内敛优化 |
| lru:24 | 复用同一 24bit:LRU 模式存最后访问时钟,LFU 模式拆成 16bit 分钟时间戳 + 8bit 计数器 |
| refcount | 引用计数回收 + 对象共享:0–9999 整数共享(OBJ_SHARED_INTEGERS = 10000),LRU/LFU 模式下共享对象时钟不更新 |
TYPE 看 type;OBJECT ENCODING key 看编码(int/embstr/raw/listpack/hashtable/intset/skiplist/quicklist);OBJECT REFCOUNT 验证整数共享。Type × Encoding Map
| type | 可能 encoding(OBJECT ENCODING 输出) | 切换条件(默认配置) |
|---|---|---|
| string | int → embstr → raw | 值是 ≤ 20 位整数(long 范围)→ int;字符串 ≤ 44 字节 → embstr;超过 → raw(浮点数按字符串存) |
| list | quicklist(节点内是 listpack) | 始终 quicklist;节点大小由 list-max-listpack-size 控制(默认 -2 = 8KB/节点) |
| hash | listpack → hashtable | 键值对数 ≤ hash-max-listpack-entries 且 value ≤ hash-max-listpack-value(64B)→ listpack;超出转 hashtable。7.x 阈值 128,8.0 起默认 512 |
| set | intset / listpack / hashtable | 全为整数且个数 ≤ set-max-intset-entries(512)→ intset;7.2 起小集合(≤128 个且元素 ≤64B)→ listpack;否则 hashtable |
| zset | listpack → skiplist | 元素数 ≤ zset-max-listpack-entries(128)且 member ≤ zset-max-listpack-value(64B)→ listpack;超出转 skiplist |
Simple Dynamic Strings · src/sds.h
| 维度 | C 字符串 | SDS |
|---|---|---|
| 取长度 | O(N):遍历到 \0 | O(1):头部 len 字段直接读(STRLEN 常量时间) |
| 二进制安全 | 不安全:内容含 \0 即被截断,只能存文本 | 二进制安全:以 len 判边界,\0 只是结尾标记;可存图片序列化字节流等任意字节 |
| 拼接溢出 | 忘记 realloc 直接缓冲区溢出(安全漏洞之源) | 拼接前检查 alloc-len,不足先 sdsMakeRoomFor 扩容,杜绝溢出 |
| 修改内存分配 | 每次改长必然 realloc | 空间预分配:扩容后新分配空间 = min(翻倍, +1MB)(SDS_MAX_PREALLOC = 1MB,即 <1MB 翻倍、≥1MB 每次多给 1MB),减少连续增长的 realloc 次数 |
| 缩短内存 | 无法安全缩短 | 惰性释放:缩短只减 len 不释放 alloc,字节留在原地,等下次写入直接复用;必要时 API 可显式释放 |
| C 兼容 | — | buf 仍以 \0 结尾,多数 <string.h> 函数可直接复用,避免重复造轮子 |
sdshdr5/8/16/32/64
| 结构体 | len/alloc 宽度 | 头部总开销 |
|---|---|---|
sdshdr5 | 无 len/alloc 字段 | 1B flags(3bit type + 5bit len) |
sdshdr8 | 1B / 1B | 3B(len+alloc+flags) |
sdshdr16 | 2B / 2B | 5B |
sdshdr32 | 4B / 4B | 9B |
sdshdr64 | 8B / 8B | 17B |
// src/sds.h · sdshdr8(长 ≤255 的字符串) struct __attribute__((packed)) sdshdr8 { uint8_t len; // 已用长度 uint8_t alloc; // 分配总长(不含头与\0) unsigned char flags; // 低 3bit 存 type char buf[]; }; // packed:禁止结构体对齐填充, // 3B 头部是"真实" 3 字节 // len 与 alloc 分开维护: // 空闲空间 = alloc - len // → 惰性释放的实现基础
| embstr(string 编码之一) | 说明 |
|---|---|
| ≤ 44 字节 → embstr | 一次分配:redisObject 头 + sdshdr8 + 44 字符 + \0 恰好装进 jemalloc 64B 分配类;连续内存对 CPU 缓存友好 |
| embstr 是只读的 | 任何修改(APPEND 等)先转 raw 再改——没有就地修改的余地(分配粒度固定) |
dict · src/dict.c
Load Factor · dict.c
| 动作 | 触发条件(负载因子 = ht[0].used / ht[0].size) | 新 size 的取法 |
|---|---|---|
| 扩容 expand | 无 BGSAVE / BGREWRITEAOF 在跑:负载因子 ≥ 1 有 BGSAVE / BGREWRITEAOF 在跑:负载因子 ≥ 5(dict_force_resize_ratio) | 第一个 ≥ used×2 的 2 的幂 |
| 缩容 shrink | 负载因子 < 0.1 | 第一个 ≥ used 的 2 的幂(多为对半缩) |
| 迁移节奏 | 每次增删改查附带迁移一个非空桶(连续跳过空桶计数 empty_visits 有限,防止纯扫空桶) | — |
| 完成 | ht[0] 迁空 → 释放 ht[0],ht[1] 变为新的 ht[0],rehashidx 置回 −1,新 ht[1] 清空待用 | — |
fork 出的子进程靠写时复制共享父进程内存页。此时激进 rehash 会大量改写 ht[0] 所在页,触发更多 COW 页复制、内存翻倍放大。提高阈值 = rehash 期间尽量不动老表,为 fork 省内存。
哈希取模优化为按位与:hash & sizemask(sizemask = size−1)。扩缩容取 2 的幂才能保证 sizemask 全 1,一次位运算完成取模——dictType 里还要求哈希函数带种子(防哈希碰撞注入攻击)。
t_zset.c · zset = dict + zskiplist
zskiplistNode · server.h
// src/server.h · Redis 7.x/8.x typedef struct zskiplistNode { sds ele; // member(共享 sds) double score; // 排序分数 struct zskiplistNode *backward; // 后退指针(仅 L0) struct zskiplistLevel { struct zskiplistNode *forward; unsigned long span; // 跨度 } level[]; // 柔性数组:层高随机 } zskiplistNode; // ZSKIPLIST_MAXLEVEL = 32(8.0 恒定) // ZSKIPLIST_P = 0.25:每层晋升概率 1/4 // 期望层高 1/(1-p)×… 平均指针 ≈1.33 // 期望查找 O(log₄N)——常数比红黑树大 // 但绝对值依然对数级
| 为什么不选红黑树/AVL | 理由 |
|---|---|
| 范围查询是主战场 | ZRANGE 类操作命中后沿 L0 链表顺序遍历即天然有序输出;树做范围要中序回溯、实现繁琐 |
| 实现与调试简单 | 无旋转、无变色;插入/删除只处理前后指针——antirez 自述选择理由 |
| 概率平衡 | 层高由随机数决定(p=0.25),期望对数高度,无需维护严格平衡 |
| span 免费送 ZRANK | 每层记录跨度,查找路径累加 span 直接得排名,ZRANK O(logN) |
| backward 只在 L0 | 反向遍历(ZREVRANGE)够用;上层不需要双向,省指针 |
7.0 的全面替换 · src/listpack.c
quicklist.c · intset.c
双向链表串起多个 listpack 节点(7.0 前是 ziplist)。纯链表指针开销大(prev/next 24B 对 8B 数据)、纯 ziplist 大块内存连锁更新 + realloc 代价高——quicklist 取中:每节点一块 ≤8KB 的紧凑内存,链表负责 O(1) 两端。
关键配置:list-max-listpack-size 负数按字节数(-1=4KB / -2=8KB 默认 / -3=16KB / -4=32KB / -5=64KB);正数 = 每节点最多 entry 数。list-compress-depth 0:两端各保留 N 个节点不压缩,中间节点 LZF 压缩——"头尾热、中间冷"的列表(如时间线)可省一半以上内存。
有序整数数组 + 二分查找。三个成员:encoding(int16/int32/int64)、length、contents。新元素比现有类型宽 → 整表升级(upgrade):重新按新宽度分配并搬移;查找 O(logN),插入保持有序。
两个边界:只升不降——删掉大整数后 encoding 不回落;只要混入一个字符串元素,整个集合升级为 hashtable,且不再转回 intset。默认上限 set-max-intset-entries 512,超过直接 hashtable。
| 追问 | 答案 |
|---|---|
| 为什么 list 不直接用双向链表? | adlist 每节点 2 指针 + 独立分配,小元素场景指针开销超过数据本身;quicklist 把 8KB 内的数据"打包",指针均摊近乎为零 |
| quicklist 节点还会连锁更新吗? | 7.0 前节点内是 ziplist,会;7.0 起节点内是 listpack,不会——这就是 7.0 换血的意义 |
| intset 二分查找为什么够快? | 上限 512 个元素,log₂512 ≈ 9 次比较;且全是整数、缓存局部性极好,紧凑数组常快过哈希 |
Config Defaults · redis.conf
| 配置(7.0+ 命名) | 默认值 | 控制对象 |
|---|---|---|
hash-max-listpack-entries | 7.x:128 → 8.0 起:512 | hash 转 hashtable 的键值对数上限 |
hash-max-listpack-value | 64 | hash 单个 value 字节数上限 |
set-max-intset-entries | 512 | 纯整数集合保持 intset 的元素数上限 |
set-max-listpack-entries / value | 128 / 64 | 小集合 listpack 编码(7.2 引入)的元素数 / 单元素字节上限 |
zset-max-listpack-entries | 128 | zset 转 skiplist 的成员数上限 |
zset-max-listpack-value | 64 | zset 单个 member 字节数上限 |
list-max-listpack-size | -2(8KB/节点) | quicklist 单节点 listpack 的大小或条目数 |
list-compress-depth | 0 | 两端不压缩的节点数,中间 LZF 压缩 |
一旦升级(hash → hashtable、zset → skiplist、set → hashtable、intset 升宽),即使之后删除元素缩回阈值以下,也不会自动降回紧凑编码。想要降回去只能新 key 重建——排查"明明数据变小了内存却不降"就是这个原因。
7.0 前叫 hash-max-ziplist-entries 等(含"ziplist"字样);7.0 起 config 改名为 *-max-listpack-*,旧名在 7.x 兼容别名。8.0 把 hash 的 entries 默认从 128 提到 512——用更大紧凑区间换更低内存。
Edge Cases
| 边界 | 展开 |
|---|---|
| embstr 的 44 从哪来 | jemalloc 64B 分配类 − robj 16B − sdshdr8 头 3B − \0 1B = 44。embstr 一次分配、缓存友好、但只读:APPEND/INCR 等修改先转 raw——"44"比"embstr 更省"更值钱 |
| 整数对象共享 | 0–9999(OBJ_SHARED_INTEGERS=10000)的整数字符串全局共享一份;refcount 计数。注意:LRU/LFU 模式下共享对象不更新访问时钟(淘汰统计失真为可接受的代价) |
| 字符串能存整数吗 | 能且优先:值为 long 范围整数时编码直接 int,STRLEN/OBJECT ENCODING 验证;INCR 系列只对 int 编码生效,非整数报错——这就是"计数器要用 INCR 而非 GET+SET"的底层原因之一(原子 + 编码保证) |
| 大 key 与编码的关系 | 大 key 常见成因之一是越过紧凑编码阈值(10 万元素的 hash 直接 hashtable)。治理思路:分桶(hash tag + 分片 key)让每个 hash 回到 listpack 区间,内存可省数倍 |
Cheat Sheet
| SDS | 长度头 + 字节数组:O(1) 取长度、二进制安全、预分配 + 惰性释放 |
| dict | 两张表 + rehashidx:渐进式 rehash 把一次 O(N) 搬家摊成每次 O(1) |
| skiplist | 多层链表 + span:范围/排名 O(log N),实现比平衡树简单,ZRANK 免费 |
| listpack | 连续内存、entry 自包含(不记前驱长度)→ 无连锁更新,7.0 起取代 ziplist |
| quicklist / intset | quicklist = 链表节点内挂 listpack(默认 8KB/节点);intset = 有序整数数组 + 二分 |
| string | 整数(long 内)→ int;≤44B → embstr;否则 raw |
| hash / zset | 元素数 ≤ 阈值 且 每个值 ≤64B → listpack;否则 hashtable / skiplist |
| set | 全整数且 ≤512 → intset;小集合(7.2+)→ listpack;否则 hashtable |
| list | 恒为 quicklist(节点内是 listpack) |
| 44 字节 | embstr 上限 = 64B 分配类 − robj 16B − sdshdr8 头 3B − \0 1B |
| 16 字节 | robj 大小(type:4 + encoding:4 + lru:24 + refcount + ptr) |
| 0–9999 | 共享整数对象区间(OBJ_SHARED_INTEGERS=10000),共享对象不更新时钟 |
| 1MB | SDS 预分配分界:<1MB 翻倍、≥1MB 每次 +1MB |
| 128 / 64 | zset·set 紧凑编码的元素数 / 单元素字节阈值;hash 7.x 也是 128 |
| 512 | intset 元素上限;8.0 起 hash-max-listpack-entries 默认值 |
| 1 / 0.1 / 5 | 负载因子:≥1 扩容、<0.1 缩容;有 BGSAVE/AOF 重写子进程时扩容阈值 ≥5 |
| p=0.25 / 32 | 跳表晋升概率与最大层高(平均约 1.33 个指针/节点) |
| 252 | ziplist 的 prevlen 由 1B 变 5B 的临界值——连锁更新的悬崖边 |
hash & sizemask);③ intset 宽度只升不降,混入一个字符串就整体转 hashtable。OBJECT ENCODING key 看现状 → 与阈值比对判断"是否本可以更省" → 分桶 / 拆 key 让每份回到紧凑区间后重建(元素级内存常省 50–70%)。
Interview QA · 1/2
先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的翻回上一页速查表。
16B:4bit type(五大类型)、4bit encoding(底层实现)、24bit lru(LRU 时钟或 LFU 数据)、int refcount(引用计数/共享)、void* ptr。type 定行为、encoding 定实现,二者解耦是编码优化的基础。
头部记 len 取长度 O(1);按 len 判边界所以二进制安全;拼接前检查 alloc 扩容防溢出;预分配(<1MB 翻倍、≥1MB +1MB)摊平增长成本,缩短只减 len 惰性释放;末尾仍保留 \0 兼容 C API。
扩容时持有两张表,rehashidx 记进度;每次增删改查顺带迁移一个非空桶,读双查两表、新增只进 ht[1]、定时任务 1ms 兜底。一次迁完是 O(N) 会阻塞主线程,渐进式把它摊成每次操作 O(1)。
负载因子(used/size)≥1 扩容、<0.1 缩容;有 BGSAVE/BGREWRITEAOF 时扩容阈值升到 5——子进程靠 COW 共享内存,少动老表可减少页复制放大。新 size 取 2 的幂以支持 hash & sizemask 位运算取模。
dict 负责 member→score 的 O(1) 点查(ZSCORE,ZADD 更新前定位旧分);skiplist 按 score 有序支撑 ZRANGE/ZRANK 等范围与排名操作。member 的 sds 两结构共享不复制——用双倍内存换两种复杂度各取最优。
①范围查询命中后沿最底层链表顺序遍历天然有序,树要繁琐中序处理;②无旋转无变色,实现调试简单(antirez 自述);③概率平衡 p=0.25 期望对数高度且省内存;④每层 span 累加直接算排名,ZRANK 免费。
ziplist 每个 entry 头记前驱长度(≤252 占 1B,否则 5B):中间元素变大迫使后继扩容后移,可级联全表、最坏 O(N²)。listpack 的 entry 只记自身,不引用前驱——无连锁。
64(jemalloc 分配类)− 16(robj)− 3(sdshdr8 头)− 1(\0)= 44。一次分配装下对象头+字符串,缓存友好;但没有就地修改空间,任何修改先转 raw,这就是 embstr "只读"的原因。
Interview QA · 2/2
同样先自答。这一组的答案都要落到具体数值或版本——只说"会自动转换"是不及格的。
string:int、embstr、raw;hash/zset 小数据:listpack(≤6.2 是 ziplist);hash/set 大数据:hashtable;set 整数:intset;zset 大数据:skiplist;list:quicklist。排查编码是内存优化的第一步。
不是。超阈值升级后(hash→hashtable、zset→skiplist、set→hashtable、intset 升宽),即使删除元素缩回阈值以下也不自动降级。要回收只能重建 key。"数据变少内存不降"的经典原因。
7.x 默认 128 个键值对、value 上限 64B;Redis 8.0 起把 entries 默认提到 512,扩大紧凑编码适用面。同时 7.0 起配置名从 *-max-ziplist-* 改为 *-max-listpack-*,背旧名会露馅。
双向链表串多个 listpack 节点(7.0 前 ziplist),兼顾 O(1) 两端与紧凑内存。负数按字节:-1=4KB、-2=8KB(默认)、-3=16KB、-4=32KB、-5=64KB;正数是每节点最多条目数。list-compress-depth 可 LZF 压缩中段。
不支持降级:存过 int64 后删掉大数 encoding 仍是 int64。混入任意非整数元素,整个集合升级为 hashtable 且不再转回。上限默认 512 个(set-max-intset-entries),超过直接 hashtable。
rehash 进行中不再次触发扩容(等本次完成);所有新增键一律写 ht[1],保证 ht[0] 只减不增最终迁空;读和删要双表;rehashidx 之前的桶必为空,定位时可跳过。
span 是本层 forward 指针跨过的节点数,查找路径上累加 span 即为排名,ZRANK 因此 O(logN);backward 只存在于最底层,供 ZREVRANGE 反向遍历,上层不设反向指针以省内存。
超大 hash 越过阈值变 hashtable:每对键值付出 dictEntry+指针+重哈希开销。按业务键分桶(hash tag 或取模拆 key),让每个 hash 保持小规模走 listpack 编码,元素级内存省一半以上,还能配合 expire 整桶过期。
Related & References
过期删除与内存淘汰 → robj 的 lru:24 字段在此展开
单线程模型 → 高效数据结构是"快"的四要素之一
RDB / AOF 持久化 → 编码紧凑度决定 RDB 体积与恢复速度
主从与集群 → 大 key 对同步与迁移的影响
参考来源(★ = 最值得原文精读的一篇)
| ★ redis 源码 src/dict.c + src/dict.h | 渐进式 rehash 全流程可通读:dictht/rehashidx、dict_force_resize_ratio、_dictRehashStep、dictRehashMilliseconds——本 deck 最值得逐行看的一份 |
| src/server.h | robj 字段、zskiplistNode(level/span/backward)、ZSKIPLIST_P=0.25、MAXLEVEL=32、EMBSTR 上限 44 |
| src/sds.h / sds.c | sdshdr5/8/16/32/64 分级头部、SDS_MAX_PREALLOC=1MB、sdsMakeRoomFor |
| src/t_zset.c · listpack.c · quicklist.c · intset.c | zset 双结构、listpack entry 布局、quicklistNode 与 LZF、intset 升级 |
| redis.io/commands/object-encoding + 数据类型文档 | 各类型合法编码输出与阈值行为的官方口径 |
| redis.conf(8.0 / 8.2)+ 7.0 RELEASENOTES | 阈值默认值核对(hash 8.0 起 512);listpack 全面替代 ziplist |