Redis的ZSet对象底层原理——跳表

我们来聊聊「跳表(Skip List)」,这是一个既经典又优雅的数据结构,尤其在 Redis 中非常重要,比如 ZSet(有序集合)底层就用到了跳表。


🌟 跳表(Skip List)简介

跳表是一种有序链表的升级版,通过引入多级"索引层"来加快查找效率。

✅ 主要特点:

特性 说明
有序结构 跳表维护的是一个按 key 排序的链表
多级索引 类似高速公路入口,有"快捷路径"
查找时间复杂度 O(log n),跟平衡树差不多
实现简单 不像红黑树那样复杂
支持范围查找 很适合 Redis 中的范围查询场景

📐 跳表结构长啥样?

假设我们有以下数据:[1, 3, 5, 7, 9]

我们构建跳表的过程如下(用 表示链表):

复制代码
Level 2:      1  →         →       → 9
Level 1:      1  →    5    →       → 9
Level 0:      1  → 3 → 5 → 7 → 9

你可以把它想象成"高架桥+地面道路"的组合:

  • 第 0 层是原始链表,所有节点都在这里
  • 每上一层都是稀疏的抽样索引,跳过一些中间节点,加快查找速度
  • 查找从最上层开始,逐层向下,"跳跃式前进"

🔍 查找过程举例:查找 7

  1. 从最上层的 1 开始 → 跳到 9(9 > 7,不行)
  2. 回退到下一层 → 1 → 5 → 9(9 > 7,继续往下)
  3. 到最底层 → 5 → 7 ✅ 找到!

→ 相比普通链表要顺着一格一格走,跳表"跳着走",很快!


📈 跳表的时间复杂度

操作 时间复杂度
查找 O(log n)
插入 O(log n)
删除 O(log n)
空间复杂度 O(n)

相比红黑树、AVL 树,它的代码实现更简单、更易于维护。


💡 跳表的应用场景

场景 用途
Redis ZSet 支持快速插入、范围查询(如 zrangebyscore)
LSM-Tree(如 RocksDB) 早期实现用过跳表
时间序列数据 有序插入、范围检索
分布式系统 一致性哈希环也可用跳表改进检索速度

🔧 跳表 vs 红黑树

项目 跳表 红黑树
插入复杂度 O(log n) O(log n),但旋转逻辑复杂
实现难度 简单 复杂(需维持平衡)
查询性能 相似 相似
空间开销 稍高(多指针) 较少

🧪 小技巧:跳表的层数怎么选?

跳表是随机化结构,每次插入一个节点时,以一定概率(如 1/2)决定它是否晋升到上一层。

最常见的方式是:

c 复制代码
while (random() < p && level < MAX_LEVEL) {
    level++;
}

所以跳表是一种「概率平衡」的数据结构。


📦 总结

优点 缺点
实现简单、性能稳定 占用内存较多
支持快速范围查找 查询时间有轻微波动
非常适合缓存、有序集合 不如 B 树适合磁盘存储

在 Redis 中,跳表(Skip List) 主要用于 Sorted Set(有序集合,zset) 的底层实现。


✅ 使用跳表的场景:zset(有序集合)

Redis 的 zset 类型是一个既可以快速查找元素、又能按照分数排序的数据结构。为了实现这个特性,它使用了 两种结构 来共同实现:

结构 作用
哈希表(dict) 用于通过成员名快速定位其分数,O(1) 时间复杂度
跳表(skiplist) 用于按照分数进行有序排列,支持范围查找、按排名查找等,平均 O(log n) 复杂度

🧠 为什么 Redis 用跳表而不是红黑树?

虽然红黑树、AVL 树等自平衡二叉树也能满足排序要求,但 Redis 选择跳表是因为:

  1. 实现简单、易于维护
  2. 插入和删除操作更平滑,少量随机性避免最坏情况
  3. 天然支持范围查询和按 rank 查找
  4. 跳表是线性结构,遍历更快,利于内存预取(cache friendly)

🧪 举个例子:

bash 复制代码
127.0.0.1:6379> ZADD myzset 1 a 2 b 3 c
127.0.0.1:6379> ZRANGE myzset 0 -1 WITHSCORES
1) "a"
2) "1"
3) "b"
4) "2"
5) "c"
6) "3"

在内部:

  • Redis 用 dict 维护 a → 1b → 2c → 3
  • 同时用跳表按分数 1 → 2 → 3 排列节点,方便范围查询,比如 ZRANGEBYSCORE

🔧 Redis 跳表结构核心代码(在源码中)

源码路径:/src/t_zset.c

关键结构:zskiplistzskiplistNode

c 复制代码
typedef struct zskiplistNode {
    robj *obj;         // 成员对象
    double score;      // 分数
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

✅ 总结

Redis 中只有 zset 使用跳表,结合 hash dict 实现高效的查找和排序。

相关推荐
别来无恙1492 小时前
深入解析MySQL中的HAVING关键字:从入门到实战
数据库·mysql
雷神乐乐2 小时前
Oracle常用函数
数据库·sql·oracle
luckyext7 小时前
SQL的RAND用法和指定生成随机数的范围
运维·服务器·数据库·sql·sqlserver·运维开发·mssql
清幽竹客7 小时前
redis 进行缓存实战-18
redis·缓存
文牧之8 小时前
Oracle 的 ALTER DATABASE RECOVER MANAGED STANDBY DATABASE FINISH 命令
运维·数据库·oracle
数据库幼崽8 小时前
MySQL 8.0 OCP 1Z0-908 191-200题
数据库·mysql·ocp
萤火夜8 小时前
MYSQL之复合查询
数据库·mysql
JWenzz18 小时前
Redis删除策略
java·数据库·redis·缓存
知识中的海王9 小时前
Chrome 缓存文件路径
chrome·缓存
江畔柳前堤9 小时前
PyQt学习系列07-数据库操作与ORM集成
数据库·学习·算法·机器学习·架构·pyqt