ZSet 跳表与内存拐点
有序集合需要同时支持「按成员查分数」和「按分数排序取区间」两类访问,因此内部用了两套结构:一个字典负责成员到分数的映射,一个跳表负责按分数有序排列。当元素少且小时,Redis 用紧凑的 listpack 把成员与分数连续存放,省掉两套结构的指针开销;一旦元素数或成员长度超过阈值,就升级为跳表加字典的组合,查询仍然高效但内存明显上升。
基本信息
| 两种编码 | listpack(紧凑)与 skiplist(跳表加字典) |
|---|---|
| 条目阈值 | zset-max-listpack-entries 默认 128 |
| 成员长度阈值 | zset-max-listpack-value 默认 64 字节 |
| 复杂度 | 插入与查找 O(log N),区间取数为 O(log N + M) |
| 查看方式 | OBJECT ENCODING key |
典型使用场景
- 排行榜容量评估:估算元素数与成员长度,判断榜单是否已经越过编码拐点
- 内存优化:把长成员名换成短标识,让 ZSet 保持在紧凑编码而不必调整阈值
- 延时队列与限流:元素数通常不大,多处于 listpack 编码,内存可控
- 大批量导入前的预判:一次性写入数十万成员会立刻触发升级,内存占用与预期差距很大
- 排查内存异常上涨:确认是否因为新增成员触发编码切换而非数据量本身增长
观察编码与阈值
观察编码与阈值
redis-cli ZADD rank 100 alice 99 bob
redis-cli OBJECT ENCODING rank # listpack
redis-cli CONFIG GET zset-max-listpack-entries # 默认 128
redis-cli CONFIG GET zset-max-listpack-value # 默认 64
redis-cli ZADD rank 1 '一个远超六十四字节的长成员名称用于触发编码切换'
redis-cli OBJECT ENCODING rank # 已升级为 skiplist
redis-cli MEMORY USAGE rank # 对比升级前后的实际占用编码与开销
| 编码 | 存储方式 | 内存特点 |
|---|---|---|
| listpack | 成员与分数连续存放 | 无额外指针,内存最省,查找为线性扫描 |
| skiplist | 跳表加字典双结构 | 每个成员存在于两处,内存上升明显 |
| 切换条件 | 条目数或成员长度超阈值 | 任一条件满足即升级,且不可逆 |
| 影响面 | 升级只针对该键 | 同一次批量写入中的大键会立即升级 |
控制成员长度比调阈值更有效:阈值调大会让单键包含更多元素,虽然省了指针开销,但对该键做范围查询或整键删除时的单次阻塞时间也随之变长。把成员名从长文本换成紧凑编号,既保持紧凑编码又不增加单次操作负担。
批量写入超大 ZSet 会长时间占用主线程:一条 ZADD 携带数十万对成员值在服务端是单命令执行,期间其他请求全部排队。导入大批量数据应拆成多次小批量写入,并在低峰执行。