Redis 跳表(zset 底层核心)完整教程
作者:超梦dasgg
Redis 是一种高性能的键值对数据库,支持多种类型的数据结构,包括字符串、列表、集合、有序集合等,本文介绍Redis跳表(zset底层核心)完整教程,感兴趣的朋友跟随小编一起看看吧
Redis 中跳表 SkipList 只用于有序集合 zset,是 zset 的底层核心数据结构之一(另一个是 dict 哈希表,存 member→score 映射)。
跳表(Skip List)的基本概念
跳表是一种概率性的数据结构,它允许在有序集合中快速查找、添加、删除元素。跳表通过维护多个层次的链表来实现,每个层次的链表包含上一层链表中一部分元素。这种设计使得跳表能够在O(log n)的时间复杂度内完成搜索、插入和删除操作。
一、为什么 Redis 不用平衡树,选择跳表?
平衡树(红黑树、AVL):
- 查找 / 插入 / 删除 O (logn),但节点内存、旋转操作、递归逻辑复杂,CPU 缓存不友好(树节点分散)。
跳表优势:
- 同样平均时间复杂度
O(logn); - 全顺序链表实现,无旋转、无递归,代码极简;
- 多层链表,范围遍历极其高效(zrange/zrevrange 高频场景);
- 随机内存布局更少,缓存命中率优于树;
- 方便实现按分值区间、按排名快速查询(zrank、zrangebyscore)。
二、跳表基础原理
1. 普通有序链表缺陷
单链表:查找元素必须从头遍历,最坏 O (n)。
2. 跳表多层索引优化
- 底层第 0 层:完整有序链表,存全部真实数据节点(按 score 升序);
- 上层多层索引:稀疏抽取部分节点作为 “高速通道”;
- 查询时从最高层往下跳,快速缩小范围,再下沉底层精准匹配。
示例逻辑:
L3: head → 10 → 50 → nil L2: head → 10 → 30 → 50 → 90 → nil L1: head → 5 → 10 → 20 → 30 → 50 → 70 → 90 → nil L0: head→1→5→7→10→18→20→30→44→50→66→70→90→nil
查找 44:L3→50 太大下 L2→30 太小下 L1→50 太大下 L0,找到 44。
3. 层数随机生成(Redis 核心策略)
插入节点时通过幂次随机算法生成层高:
- 1/2 概率层数 = 1
- 1/4 概率层数 = 2
- 1/8 概率层数 = 3 …… 数学期望平均层高仅 2,空间开销可控。 Redis 最大限制层高
ZSKIPLIST_MAXLEVEL = 32,支撑千万级数据无压力。
三、Redis 跳表源码结构体(redis 6/7 统一)
1. 跳表节点 zskiplistNode
typedef struct zskiplistNode {
// zset 元素值,唯一字符串
sds ele;
// 排序分值,double
double score;
// 后退指针,仅第0层有,用于 zrevrange 逆序遍历
struct zskiplistNode *backward;
// 多层前进指针数组,level[0]底层,level[1]第一层索引...
struct zskiplistLevel {
struct zskiplistNode *forward; // 同层下一个节点
unsigned long span; // 当前节点到forward节点中间的元素个数(用于快速算排名 zrank)
} level[];
} zskiplistNode;关键字段说明:
backward:逆序遍历优化,不用反向多层回溯;span:每层跨度,累加 span 直接得到元素排名,不用遍历计数;- 柔性数组
level[]:每个节点层高不同,节省内存。
2. 跳表总结构 zskiplist
typedef struct zskiplist {
// 头/尾节点
struct zskiplistNode *header, *tail;
// 当前最大有效层高(不包含空的32层)
unsigned long level;
// 总元素数量
unsigned long length;
} zskiplist;3. zset 整体结构 zobj
typedef struct zset {
dict *dict; // hash表:member -> score,O(1)查分值、判重
zskiplist *zsl; // 跳表:按score有序存储,范围查询、排序、排名
} zset;双结构配合:
zadd key member score:先 dict 判断 member 是否存在(去重),再更新跳表;zscore key member:直接查 dict,O (1);zrange/zrank/zrangebyscore:走跳表,高效区间操作。
四、核心操作时间复杂度
设元素总数 n,层高平均 logn:
- 查找元素(按 score/member)
- zscore:O(1)(dict)
- 按分值定位节点:O (logn)(跳表)
- zadd 插入:O(logn)
- 随机层高、多层找前驱节点、更新 span、链表插入
- zrem 删除:O(logn)
- zrank/zrevrank 查排名:O (logn)(累加 span,无需遍历)
- zrange/zrevrange 区间遍历:O(logn + k)
- logn 定位起点,k 是返回元素数量;远优于红黑树范围查询
- zrangebyscore 按分值区间:O(logn + k)
五、Redis 跳表独有的特性(通用跳表没有)
- 支持重复 score 多个 member 可以拥有相同 score;排序规则:先比 score,score 相等则按字符串 ele 字典序升序。
- span 跨度字段快速计算排名 普通跳表无跨度,统计排名需要遍历;Redis 通过 span 累加直接得到 rank。
- backward 后退指针 底层链表双向,逆序遍历 zrevrange 性能大幅提升。
- 最大层高固定 32,理论可容纳 2^64 个节点,业务完全够用。
- 随机层高算法经过工程优化,不会出现极端单层退化。
六、常见面试考点
1. zset 为什么同时用 dict + 跳表,只存一个不行?
- 只用跳表:查询某个 member 的 score 需要 O (logn),频繁 zscore、zadd 判重性能差;
- 只用 dict:无法有序遍历、按分值范围查询、获取排名;
- 两者互补,空间换时间,覆盖 zset 所有操作场景。
2. 跳表 vs 红黑树 zset 选型对比
| 特性 | Redis 跳表 | 红黑树 |
|---|---|---|
| 范围遍历 | 极快,链表连续遍历 | 较慢,中序遍历递归 |
| 实现复杂度 | 低,纯链表无旋转 | 高,平衡旋转逻辑多 |
| 内存开销 | 平均层高 2,很小 | 每个节点左右 + 父节点指针 |
| 逆序查询 | 原生 backward 支持 | 需要额外处理 |
| 随机插入删除 | O (logn),无锁复杂操作 | O (logn),频繁旋转 |
3. 跳表会不会退化成链表?
理论存在概率,但 Redis 随机层高是指数递减,百万数据下几乎不可能全部节点只有 1 层;极端退化概率可以忽略,生产无风险。
4. zrange 底层怎么走?
- 通过 score 在跳表从上至下二分定位起始节点(O (logn));
- 沿着第 0 层 forward 指针顺序遍历 k 个节点返回;
- 逆序 zrevrange 利用 backward 指针反向遍历。
七、简单插入流程梳理(zadd)
- 通过 dict 判断 member 是否已存在,存在则先删除旧跳表节点;
- 随机生成新节点层高 random_level;
- 从最高层向下遍历,记录每一层待插入位置的前驱节点,同时更新每层 span;
- 创建 zskiplistNode,填充 ele、score;
- 逐层插入到对应前驱节点后方,修正 forward、span;
- 处理 backward 双向指针;
- 更新跳表 length、level;
- dict 写入 member→score 映射。
八、补充:适用场景
所有依赖有序、范围、排名的场景底层都是这套跳表:
- 排行榜(积分排序、实时排名 zrank)
- 延迟队列(score 存时间戳,zrangebyscore 取到期任务)
- 分页有序列表、区间筛选数据
到此这篇关于Redis 跳表(zset 底层核心)完整教程的文章就介绍到这了,更多相关Redis 跳表zset内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
