ZSet 底层跳表原理
候选人小周在阿里的三面中,面试官拿起简历上"精通 Redis"这一行,说:
"来,画一下跳表的数据结构,说说为什么 Redis 用跳表而不是红黑树。"
小周在白纸上画了一个多层链表,面试官看了一眼:"你这个层数是怎么决定的?"
小周说:"呃...随机?"面试官:"说详细点。"
小周:"就是随机生成层数..."面试官追问:"那为什么这样设计是 O(log n) 的?"
小周开始擦汗。
【面试官心理】 这道题我用来区分 P6 和 P7。能把跳表画出来的占 50%,能解释层数随机化原理的占 20%,能说出 Redis 为什么选跳表而不是 B+Tree 或红黑树的只有 10%。跳表是 Redis 中最优雅的数据结构设计之一,值得深入理解。
一、跳表是什么 🔴
1.1 问题拆解
第一层:为什么需要跳表?
普通有序链表查询是 O(n):
查找 15,需要遍历:1 → 3 → 7 → 11 → 15,共 5 次。
跳表的核心思想:给链表建索引,多层跳跃
查找 15:L2: 1 → 7 → (超过15, 回退) → L1: 11 → 15,只需要 4 次。
1.2 ❌ 错误示范
候选人原话:"跳表就是用随机层数建多级索引,查询是 O(log n)。"
问题诊断:
- 只说了结论,没说为什么是 O(log n)
- 不知道层数随机化的具体概率分布
- 不理解跳表的空间复杂度
面试官内心 OS:"这个候选人肯定是在背题,根本没有动手推算过跳表的复杂度。"
二、跳表结构详解 🔴
2.1 节点结构
2.2 跳表结构
【面试官心理】 我让他画跳表结构,其实是想看他对"层"的理解。很多候选人能画出多层,但说不清楚每层的前向指针和 span 是什么意思。Redis 的跳表实现中,span 是用来计算 rank(排名)的,这个细节很少有人注意到。
三、层数随机化 🔴
3.1 为什么用随机层数?
这是跳表最核心的设计:用概率代替确定性。
层数生成概率分布:
3.2 为什么是 O(log n)?
这是面试官最爱的追问。让我从数学上证明:
假设每层节点数是上一层的 1/p(即 p = 4,概率 0.25):
k = logₚ(n),即层数是 O(log n)。
查询时每层最多遍历 p 个节点(因为节点间距是 p),总复杂度 = O(log n) × p = O(log n)。
3.3 标准回答
跳表层数随机化的本质是用抛硬币(概率)代替掷骰子(确定性):
【面试官心理】 这个追问我能看出候选人有没有数学思维。能推导出 O(log n) 的占 15%,能用生活类比(抛硬币)讲清楚的占 10%。更重要的是,我想看他有没有想过"为什么不固定层数"——固定层数就变成了确定性索引,需要维护平衡,而随机化用"概率保证平衡"而不是"规则强制平衡",这是算法设计的精髓。
四、Redis 为什么选跳表 🔴
4.1 追问链
面试官追问:Redis 为什么用跳表而不是红黑树或 B+Tree?
这是 P6/P7 的核心分水岭。
4.2 Redis 的取舍
Redis 作者 antirez 选择跳表的核心原因:
4.3 ❌ 错误示范
候选人原话:"跳表比红黑树快,因为它是链表。"
问题诊断:
- 把"链表"等同于"慢",完全不理解跳表的索引原理
- 混淆了普通链表和跳表的区别
- 不理解时间复杂度的含义
面试官内心 OS:"这个候选人肯定没有深入理解跳表和红黑树的核心差异,只是在背结论。"
【面试官心理】 这道题我想考察的是候选人的"算法选型能力"。能列出对比表的占 40%,能说出 Redis 作者取舍原因的占 15%,能结合 Redis 的单线程模型解释"并发友好"的占 5%。Redis 是单线程的,所以不需要复杂的锁机制,跳表的简单性在这里是巨大优势。
五、空间复杂度 🟡
5.1 跳表内存开销
每个节点的层数是随机的,期望层数是:
平均每个节点有 4 层指针,额外内存开销约为 3 个指针/节点。
相比之下,红黑树的额外开销是 2 个指针 + 1 个颜色位。
5.2 Redis 的优化
跳表的空间开销约为 3x 指针,而 B+Tree 的空间开销约为 0.5x 页结构。但 Redis 的数据量通常远小于数据库,所以跳表的额外内存开销是可以接受的。
六、ZSet 的双重索引 🟡
6.1 ZSet 同时使用跳表和哈希表
这是 Redis 的经典设计:
为什么需要两个索引?
dict 提供 O(1) 的 member→score 查询,zsl 提供有序的 score 遍历。
6.2 ❌ 错误示范
候选人原话:"ZSet 用跳表就够了,不需要哈希表。"
面试官内心 OS:"这个候选人没有理解 Redis 的设计哲学——永远用最合适的数据结构达到 O(1) 或 O(log n) 的操作复杂度,而不是为了省内存牺牲性能。"
【面试官心理】 ZSet 的双重索引是我在 Redis 面试中最喜欢追问的点。能说出 dict + zsl 双重结构的占 30%,能解释为什么要用两个索引而不是一个的占 10%。这个设计完美体现了"空间换时间"和"用合适的结构做合适的事"的原则。
七、生产避坑
:::warning ⚠️ 生产环境中的跳表翻车点:
-
ZSet 元素过多导致性能退化:ZSet 从 ziplist 转成 skiplist 后,插入复杂度从 O(1) 变成 O(log n),且元素越多跳表层数越高。
-
大 Key 问题:某个 ZSet(如热门游戏的排行榜)可能包含上百万个元素,内存占用可达数百 MB。
-
热 Key 问题:排行榜这种 ZSet 的查询 QPS 可能高达几十万,单节点成为瓶颈。 :::
排查方法:
:::tip 💡 ZSet 性能优化建议:
- 合理设置
zset-max-ziplist-entries(默认 128),延缓编码转换 - 对超大型排行榜使用分桶策略:按 score 区间分到不同的 ZSet
- 热 Key 问题用 Redis Cluster 水平扩展 :::
【面试官心理】 这道题我最终想验证的是候选人的"全局工程能力"。能画出跳表的占 40%,能讲清楚层数随机化原理的占 20%,能解释 Redis 为什么选跳表的占 15%,能说出 ZSet 双重索引设计的占 10%。能把原理和工程结合的,基本都是 P6+。