# Redis 中跳表的实现原理是什么?

9 min read
Table of Contents

Redis 中跳表的实现原理是什么?

Redis 跳表(Skip List)实现原理详解

【核心定义】

跳表是一种基于有序链表、通过添加多级索引实现快速查找、插入和删除的概率平衡数据结构,在Redis中主要用于实现有序集合(Sorted Set)的底层存储。

【关键要点】

  1. 数据结构分层:跳表由多层链表组成,底层(L0)包含所有元素,上层每层都是下层的“快速通道”,节点随机确定层高。
  2. 时间复杂度O(log n):通过多级索引,将查找复杂度从链表的O(n)优化到近似平衡树的O(log n),平均查找路径长度显著减少。
  3. 空间换时间:额外存储索引节点(约n/2个节点),空间复杂度O(n),但实际内存占用远低于平衡树。
  4. 概率平衡机制:节点层数由随机算法决定(幂次定律),避免严格平衡的开销,实现简单高效。
  5. 支持范围查询:利用有序链表特性,可以高效实现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)

  1. 从最高层header开始,向右遍历找到最后一个小于目标score的节点
  2. 下降一层继续查找,重复直到L0层
  3. 平均查找路径长度:1/p * log_{1/p}(n) ≈ 1.33 * log(n)

Step 3 - 插入过程(ZADD)

  1. 查找插入位置,记录每层的前驱节点update[]和跨度rank[]
  2. 随机生成新节点层高level
  3. 调整前后指针,更新跨度信息
  4. 如果新层高>当前跳表层数,更新跳表level字段

Step 4 - 删除过程(ZREM)

  1. 查找要删除的节点,记录每层的前驱节点
  2. 调整前驱节点的forward指针,跳过被删除节点
  3. 更新跨度信息
  4. 释放节点内存

性能优化细节

  • 跨度(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选择跳表而不是红黑树实现有序集合?

技术事实:

  1. 实现简单:跳表代码约200行,红黑树实现复杂且容易出错
  2. 范围查询高效:跳表的双向链表特性天然支持ZRANGE操作
  3. 并发优化潜力:跳表更容易实现无锁(lock-free)并发版本
  4. 实际性能相当:对于内存操作,两者性能差异在可接受范围内
  5. 历史原因: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跳表如何支持相同分数的元素?

实现机制:

  1. 主要排序键:double类型的score,64位浮点数
  2. 次要排序键:成员字符串的字典序(lexicographic order)
  3. 查找逻辑:先按score查找,score相同时比较成员字符串
  4. 保证确定性:相同的元素集合总是得到相同的排序结果

Q5:跳表的跨度(span)字段有什么作用?

核心功能:

  1. 快速排名计算:ZRANK命令可在O(log n)时间内计算元素排名
  2. 范围查询优化:ZRANGE start end可以直接定位,无需遍历
  3. 实现原理:每层span记录到下一个节点的距离,从header开始累加span得到排名

【版本差异】

  • Redis 3.2之前:有序集合在元素较少时使用ziplist编码,较多时使用跳表+字典
  • Redis 3.2+:引入quicklist替代双向链表,但跳表实现保持稳定
  • Redis 7.0:优化内存布局,减少内存碎片,跳表核心算法不变
  • 配置参数:zset-max-ziplist-entries和zset-max-ziplist-value控制编码转换阈值

【实战启示】

当面试官问及跳表时,可自然延伸到:

  1. Redis有序集合的实际应用场景(排行榜、延迟队列)
  2. 为什么LevelDB/RocksDB也使用跳表作为MemTable
  3. 如何基于跳表思想设计自己的有序数据结构
  4. 跳表在并发环境下的优化思路(无锁跳表)

这种回答结构既展示了底层原理的深度理解,又体现了知识体系的完整性,符合高级工程师的技术视野。

My avatar

文章看完了,不打算去别的篇章转转吗?👀 感谢你的耐心阅读!如果你有问题想探讨,或者发现了文中的 Bug,请务必通过页脚的链接联系我


More Posts

# Redis 中有哪些内存淘汰策略?

10 min read

Redis 内存淘汰策略是指当 Redis 实例使用的内存达到 maxmemory 配置的阈值时,Redis 根据预设的算法自动删除部分键值对以释放内存空间,从而保证新数据能够写入的一系列机制。

Read

Comments