# Redis 中跳表的实现原理是什么?
9 min read
Table of Contents
Redis 中跳表的实现原理是什么?
Redis 跳表(Skip List)实现原理详解
【核心定义】
跳表是一种基于有序链表、通过添加多级索引实现快速查找、插入和删除的概率平衡数据结构,在Redis中主要用于实现有序集合(Sorted Set)的底层存储。
【关键要点】
- 数据结构分层:跳表由多层链表组成,底层(L0)包含所有元素,上层每层都是下层的“快速通道”,节点随机确定层高。
- 时间复杂度O(log n):通过多级索引,将查找复杂度从链表的O(n)优化到近似平衡树的O(log n),平均查找路径长度显著减少。
- 空间换时间:额外存储索引节点(约n/2个节点),空间复杂度O(n),但实际内存占用远低于平衡树。
- 概率平衡机制:节点层数由随机算法决定(幂次定律),避免严格平衡的开销,实现简单高效。
- 支持范围查询:利用有序链表特性,可以高效实现ZRANGE、ZREVRANGE等范围操作。
【深度推导/细节】
节点结构与随机层高算法
// Redis 7.0 跳表节点结构(简化)typedef struct zskiplistNode { sds ele; // 成员对象(字符串) double score; // 分值(排序依据) struct zskiplistNode *backward; // 后退指针(双向链表) struct zskiplistLevel { struct zskiplistNode *forward; // 前进指针 unsigned long span; // 跨度(到下一个节点的距离) } level[]; // 柔性数组,层高随机} zskiplistNode;
// 跳表结构typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; // 节点数量 int level; // 当前最大层数} zskiplist;核心算法逻辑拆解
Step 1 - 随机层高生成(核心概率机制)
// Redis 跳表层数随机算法int zslRandomLevel(void) { int level = 1; // 0xFFFF = 65535,每次有1/4概率增加层数 while ((random() & 0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) level += 1; return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;}- 设计原理:每增加一层的概率为1/4(ZSKIPLIST_P = 0.25)
- 层数分布:L1层100%,L2层25%,L3层6.25%,L4层1.56%…
- 最大层数:Redis 7.0中ZSKIPLIST_MAXLEVEL = 32,可容纳2^64个元素
Step 2 - 查找过程(ZRANK/ZSCORE)
- 从最高层header开始,向右遍历找到最后一个小于目标score的节点
- 下降一层继续查找,重复直到L0层
- 平均查找路径长度:1/p * log_{1/p}(n) ≈ 1.33 * log(n)
Step 3 - 插入过程(ZADD)
- 查找插入位置,记录每层的前驱节点update[]和跨度rank[]
- 随机生成新节点层高level
- 调整前后指针,更新跨度信息
- 如果新层高>当前跳表层数,更新跳表level字段
Step 4 - 删除过程(ZREM)
- 查找要删除的节点,记录每层的前驱节点
- 调整前驱节点的forward指针,跳过被删除节点
- 更新跨度信息
- 释放节点内存
性能优化细节
- 跨度(span)字段:记录每层两个节点之间的距离,实现O(log n)的排名查询(ZRANK)
- 后退指针(backward):构成双向链表,支持逆序遍历和范围查询
- 分数相同处理:score相同时按成员字典序排序,保证确定性
【关联/对比】
Redis跳表 vs 平衡树(AVL/红黑树)
| 维度 | Redis跳表 | 平衡树(红黑树) |
|---|---|---|
| 实现复杂度 | 简单(约200行代码) | 复杂(插入删除需旋转) |
| 范围查询 | 天然有序,双向链表支持 | 需要中序遍历 |
| 并发友好度 | 更容易实现无锁并发 | 需要复杂锁机制 |
| 内存占用 | 每个节点1.33个指针(平均) | 每个节点2个指针+颜色位 |
| 性能稳定性 | 概率平衡,最坏情况O(n) | 严格平衡,稳定O(log n) |
Redis跳表 vs 其他数据库索引
- 对比B+树:跳表更适合内存数据库,B+树更适合磁盘(减少IO)
- 对比哈希表:跳表保持有序性,哈希表无序但O(1)查找
- 在Redis中的应用:有序集合(zset)同时使用跳表(范围操作)和字典(O(1)查找)
『面试官追问』
Q1:为什么Redis选择跳表而不是红黑树实现有序集合?
技术事实:
- 实现简单:跳表代码约200行,红黑树实现复杂且容易出错
- 范围查询高效:跳表的双向链表特性天然支持ZRANGE操作
- 并发优化潜力:跳表更容易实现无锁(lock-free)并发版本
- 实际性能相当:对于内存操作,两者性能差异在可接受范围内
- 历史原因:Redis作者认为跳表“有趣且值得推广”
Q2:跳表的最坏时间复杂度是多少?如何避免?
技术事实:
- 最坏情况O(n):所有节点都在同一层时退化为普通链表
- 避免机制:随机层高算法保证概率平衡,32层最大高度可支持2^64个元素
- 实际场景:在Redis配置下,出现性能问题的概率极低(< 1/2^64)
Q3:跳表的层高为什么用1/4概率而不是1/2?
数学原理:
- 1/2概率:平均层数2,索引节点与数据节点1:1 ,空间开销大
- 1/4概率:平均层数1.33,空间效率更高(Redis选择)
- 1/8概率:平均层数1.14,索引太少影响查询性能
- Redis权衡:1/4在空间和时间间取得最佳平衡
Q4:Redis跳表如何支持相同分数的元素?
实现机制:
- 主要排序键:double类型的score,64位浮点数
- 次要排序键:成员字符串的字典序(lexicographic order)
- 查找逻辑:先按score查找,score相同时比较成员字符串
- 保证确定性:相同的元素集合总是得到相同的排序结果
Q5:跳表的跨度(span)字段有什么作用?
核心功能:
- 快速排名计算:ZRANK命令可在O(log n)时间内计算元素排名
- 范围查询优化:ZRANGE start end可以直接定位,无需遍历
- 实现原理:每层span记录到下一个节点的距离,从header开始累加span得到排名
【版本差异】
- Redis 3.2之前:有序集合在元素较少时使用ziplist编码,较多时使用跳表+字典
- Redis 3.2+:引入quicklist替代双向链表,但跳表实现保持稳定
- Redis 7.0:优化内存布局,减少内存碎片,跳表核心算法不变
- 配置参数:
zset-max-ziplist-entries和zset-max-ziplist-value控制编码转换阈值
【实战启示】
当面试官问及跳表时,可自然延伸到:
- Redis有序集合的实际应用场景(排行榜、延迟队列)
- 为什么LevelDB/RocksDB也使用跳表作为MemTable
- 如何基于跳表思想设计自己的有序数据结构
- 跳表在并发环境下的优化思路(无锁跳表)
这种回答结构既展示了底层原理的深度理解,又体现了知识体系的完整性,符合高级工程师的技术视野。