Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis 跳表zset

Redis 跳表(zset 底层核心)完整教程

作者:超梦dasgg

Redis 是一种高性能的键值对数据库,支持多种类型的数据结构,包括字符串、列表、集合、有序集合等,本文介绍Redis跳表(zset底层核心)完整教程,感兴趣的朋友跟随小编一起看看吧

Redis 中跳表 SkipList 只用于有序集合 zset,是 zset 的底层核心数据结构之一(另一个是 dict 哈希表,存 member→score 映射)。

跳表(Skip List)的基本概念

跳表是一种概率性的数据结构,它允许在有序集合中快速查找、添加、删除元素。跳表通过维护多个层次的链表来实现,每个层次的链表包含上一层链表中一部分元素。这种设计使得跳表能够在O(log n)的时间复杂度内完成搜索、插入和删除操作。

一、为什么 Redis 不用平衡树,选择跳表?

平衡树(红黑树、AVL):

跳表优势:

  1. 同样平均时间复杂度 O(logn)
  2. 全顺序链表实现,无旋转、无递归,代码极简;
  3. 多层链表,范围遍历极其高效(zrange/zrevrange 高频场景);
  4. 随机内存布局更少,缓存命中率优于树;
  5. 方便实现按分值区间、按排名快速查询(zrank、zrangebyscore)。

二、跳表基础原理

1. 普通有序链表缺陷

单链表:查找元素必须从头遍历,最坏 O (n)。

2. 跳表多层索引优化

示例逻辑:

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 核心策略)

插入节点时通过幂次随机算法生成层高:

三、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;

关键字段说明:

  1. backward:逆序遍历优化,不用反向多层回溯;
  2. span:每层跨度,累加 span 直接得到元素排名,不用遍历计数;
  3. 柔性数组 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;

双结构配合

四、核心操作时间复杂度

设元素总数 n,层高平均 logn:

  1. 查找元素(按 score/member)
    • zscore:O(1)(dict)
    • 按分值定位节点:O (logn)(跳表)
  2. zadd 插入:O(logn)
    • 随机层高、多层找前驱节点、更新 span、链表插入
  3. zrem 删除:O(logn)
  4. zrank/zrevrank 查排名:O (logn)(累加 span,无需遍历)
  5. zrange/zrevrange 区间遍历:O(logn + k)
    • logn 定位起点,k 是返回元素数量;远优于红黑树范围查询
  6. zrangebyscore 按分值区间:O(logn + k)

五、Redis 跳表独有的特性(通用跳表没有)

  1. 支持重复 score 多个 member 可以拥有相同 score;排序规则:先比 score,score 相等则按字符串 ele 字典序升序。
  2. span 跨度字段快速计算排名 普通跳表无跨度,统计排名需要遍历;Redis 通过 span 累加直接得到 rank。
  3. backward 后退指针 底层链表双向,逆序遍历 zrevrange 性能大幅提升。
  4. 最大层高固定 32,理论可容纳 2^64 个节点,业务完全够用。
  5. 随机层高算法经过工程优化,不会出现极端单层退化。

六、常见面试考点

1. zset 为什么同时用 dict + 跳表,只存一个不行?

2. 跳表 vs 红黑树 zset 选型对比

特性Redis 跳表红黑树
范围遍历极快,链表连续遍历较慢,中序遍历递归
实现复杂度低,纯链表无旋转高,平衡旋转逻辑多
内存开销平均层高 2,很小每个节点左右 + 父节点指针
逆序查询原生 backward 支持需要额外处理
随机插入删除O (logn),无锁复杂操作O (logn),频繁旋转

3. 跳表会不会退化成链表?

理论存在概率,但 Redis 随机层高是指数递减,百万数据下几乎不可能全部节点只有 1 层;极端退化概率可以忽略,生产无风险。

4. zrange 底层怎么走?

  1. 通过 score 在跳表从上至下二分定位起始节点(O (logn));
  2. 沿着第 0 层 forward 指针顺序遍历 k 个节点返回;
  3. 逆序 zrevrange 利用 backward 指针反向遍历。

七、简单插入流程梳理(zadd)

  1. 通过 dict 判断 member 是否已存在,存在则先删除旧跳表节点;
  2. 随机生成新节点层高 random_level;
  3. 从最高层向下遍历,记录每一层待插入位置的前驱节点,同时更新每层 span;
  4. 创建 zskiplistNode,填充 ele、score;
  5. 逐层插入到对应前驱节点后方,修正 forward、span;
  6. 处理 backward 双向指针;
  7. 更新跳表 length、level;
  8. dict 写入 member→score 映射。

八、补充:适用场景

所有依赖有序、范围、排名的场景底层都是这套跳表:

到此这篇关于Redis 跳表(zset 底层核心)完整教程的文章就介绍到这了,更多相关Redis 跳表zset内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文