当前位置: 首页 > news >正文

Redis跳表(skiplist)底层原理浅析

引言

作为Redis有序集合(Sorted Set)的底层核心数据结构之一,跳表(Skip List)凭借其高效的增删查性能和相对简单的实现逻辑,成为Redis开发者必须掌握的知识点。今天我们就来扒一扒Redis跳表的“里里外外”,彻底搞懂它的设计思想和实现细节。


一、为什么是跳表?Redis的选择逻辑

在Redis的有序集合中,需要支持两种核心操作:

  1. 快速按分数排序(插入、删除、查找);
  2. 高效范围查询(如ZRANGE key start stop按排名取数据)。

常见的有序数据结构有平衡树(如红黑树)、哈希表、数组等,但都有明显短板:

  • 哈希表:无法保证顺序,范围查询需遍历所有元素(O(n));
  • 平衡树:虽然能保证O(logn)的增删查,但实现复杂(如红黑树需要处理旋转、颜色标记),且不支持高效的范围遍历(需中序遍历);
  • 数组:插入/删除的时间复杂度高(O(n))。

而跳表通过多层索引+概率平衡的设计,完美解决了这些问题:

  • 增删查时间复杂度均为O(logn);
  • 天然支持范围查询(底层是有序链表,可直接顺序遍历);
  • 实现逻辑简单(相比红黑树的复杂旋转操作,跳表仅需维护多层指针)。

这也是Redis选择跳表作为有序集合底层的重要原因。


二、跳表的核心结构:节点(zskiplistNode)

要理解跳表,首先得看它的“细胞”——节点结构。Redis的跳表节点定义在server.h中,核心字段如下(简化版):

typedef struct zskiplistNode {// 成员对象(如"member1")robj *ele;          // 分数值(排序依据,有序集合的核心)double score;       // 后退指针:指向前一层的同位置节点(支持反向遍历)struct zskiplistNode *backward; // 层级数组:每个元素是一层的“索引信息”struct zskiplistLevel {// 前进指针:指向当前层的下一个节点struct zskiplistNode *forward;  // 跨度:当前节点到下一个节点的“距离”(用于计算排名)unsigned long span;           } level[];  // 动态数组,长度=节点当前层数
} zskiplistNode;

字段详解:

  1. ele & score
    ele是有序集合的成员(字符串类型,唯一),score是分数(排序依据)。若两个成员分数相同,则按ele的字典序排序。

  2. level数组
    这是跳表的“灵魂”。每个元素zskiplistLevel表示节点在某一层的索引信息:

    • forward:当前层的“前进指针”,指向下一个节点;
    • span:当前节点到forward指向节点的“跨度”(中间跳过的节点数)。例如,若span=3,说明当前节点与下一个节点之间有3个被跳过的节点。

    节点的层数由level数组的长度决定。例如,若level长度为3,则该节点占据第0、1、2层(底层是第0层)。

  3. backward指针
    指向当前节点的下一层(更低层)的前驱节点。例如,若当前节点在第2层,backward指向第1层的同位置节点。它的作用是支持反向遍历(如ZREVRANGE命令)。


三、跳表的整体架构:从虚拟头节点到最大层数

跳表的整体结构可以类比“多层索引的链表”,主要由以下部分组成:

1. 虚拟头节点(header)

跳表的头节点是一个虚拟节点(不存储实际数据),作用是简化边界条件处理。它的level数组长度等于跳表的最大层数maxlevel,Redis默认32)。初始时,所有forward指针指向NULL,相当于“空索引”。

2. 底层链表(第0层)

底层链表包含跳表的所有节点,按score从小到大有序排列。这是跳表的“基础数据”,所有操作最终都需要回到这一层。

3. 高层索引(第1~maxlevel层)

每一层都是下一层的“稀疏索引”。例如,第1层的节点数约为第0层的1/2,第2层约为第1层的1/2,以此类推。这种“指数级稀疏”的结构,让跳表在查找时能快速跳过大量节点,将时间复杂度降到O(logn)。

4. 尾节点(tail)

尾节点是底层链表的最后一个节点(可能为空)。通过尾节点可以快速定位链表末尾,避免从头部遍历到末尾(时间复杂度O(n))。


四、层级随机生成:跳表的“概率魔法”

跳表的层数不是固定的,而是通过概率算法动态生成的。Redis的规则如下:

  1. 新节点初始层数level=1
  2. 循环抛“虚拟硬币”:以50%的概率决定是否增加层数(“正面”则level++,“反面”则停止);
  3. 直到level达到maxlevel(32)或抛硬币结果为“反面”时停止。

这种设计让节点的层数服从几何分布,平均层数约为log(n)(例如,当n=100万时,平均层数约17)。举个例子:

  • 50%的节点层数为1;
  • 25%的节点层数为2;
  • 12.5%的节点层数为3;
  • 以此类推,直到最高层(32层)的概率仅为1/(2^31)(几乎可以忽略)。

为什么用概率生成?
如果手动指定层数,很难保证平衡(可能某些层节点过多,某些层过少)。而概率生成能自动保证各层节点数大致呈指数级递减,从而让跳表的查找效率稳定在O(logn)。


五、跳表的核心操作:查找、插入、删除

1. 查找(Search):从高层到低层的“跳跃”

查找目标分数target的过程,就像“坐电梯下楼”:

  1. 从最高层出发:记录当前节点为x(初始为header);
  2. 逐层跳跃:在当前层,比较x.forward节点的scoretarget
    • x.forward.score < target:沿当前层的forward指针移动,并累加span(记录跳过了多少节点);
    • x.forward.score > target:下降到下一层(x = x.level[i].backward),继续查找;
    • x.forward.score == target:找到目标节点(需进一步比较ele处理分数相同的情况)。
  3. 到底层仍未找到:返回NULL

举个栗子
假设要查找分数为85的节点,跳表结构如下(层数从0到2):

层2:A(70) → C(80) → E(90)  
层1:A(70) → D(85) → E(90)  
层0(底层):A(70) → B(75) → C(80) → D(85) → E(90)

查找过程:

  • 层2:A(70)的下一节点是C(80)(<85),沿层2跳到C(80);
  • 层2:C(80)的下一节点是E(90)(>85),下降到层1;
  • 层1:C(80)的下一节点是D(85)(=85),找到目标!

2. 插入(Insert):先找位置,再“搭梯子”

插入节点的关键是找到每一层的插入位置,并更新索引:

  1. 记录“前驱节点”:类似查找过程,遍历各层,记录每一层中“最后一个小于目标分数的节点”(保存到update数组);
  2. 生成随机层数:为新节点生成随机的new_level(不超过maxlevel);
  3. 调整层级指针
    • 如果new_level超过当前跳表的maxlevel:更新跳表的maxlevel,并将update数组中超出原层数的部分指向header
    • 对于每一层i ≤ new_level:将新节点的level[i].forward指向update[i].level[i].forward,并将update[i].level[i].forward指向新节点;
    • 设置新节点的backward指针(指向update[0]的前驱节点,或NULL)。

3. 删除(Delete):找到节点,拆索引

删除节点需要先验证节点存在,再调整各层指针:

  1. 记录“前驱节点”:遍历各层,记录每一层中指向目标节点的前驱节点(保存到update数组);
  2. 验证节点:检查update[0].level[0].forward是否为目标节点(避免误删);
  3. 调整层级指针
    • 对于目标节点的每一层i:将update[i].level[i].forward指向目标节点的level[i].forward
    • 如果某一层的头节点的forward变为NULL(说明该层已空):更新跳表的maxlevel(降低最大层数);
    • 释放目标节点内存。

六、Redis跳表的优势:为什么比平衡树更香?

对比红黑树等平衡树,Redis跳表的优势非常明显:

  1. 实现简单:跳表仅需维护多层指针,无需处理复杂的旋转、颜色标记(红黑树的噩梦);
  2. 范围查询高效:底层是有序链表,配合span字段可直接计算排名(ZRANK时间复杂度O(logn)),范围遍历(ZRANGE)只需顺序访问底层链表的部分节点;
  3. 内存占用友好:跳表的稀疏索引不会像平衡树那样占用过多内存(红黑树的每个节点需要额外存储父/子节点指针);
  4. 支持双向遍历:通过backward指针,跳表可以高效实现反向范围查询(如ZREVRANGE)。

总结

Redis的跳表是一种“用空间换时间”的高效有序数据结构,通过多层索引和概率生成的层数,在保证O(logn)增删查性能的同时,天然支持范围查询。它的实现逻辑简单清晰,是Redis有序集合的“幕后功臣”。

下次使用ZADDZRANGE等命令时,不妨想想:这些操作的背后,跳表是如何通过多层索引快速定位数据的?理解跳表,能让你更深入掌握Redis的性能密码!

相关文章:

  • 关于网站开发的外文翻译有哪些网站可以免费发布广告
  • 做报废厂房网站怎么做中国教师教育培训网
  • wordpress网站静态化国外网站搭建
  • 合肥那家公司做网站百度一下你就知道移动首页
  • 织梦手机网站制作网络营销专家
  • 旅游网站需求分析武汉seo网站排名优化公司
  • 使用亮数据网页抓取API自动获取Tiktok数据
  • matlab机器人工具箱(Robotics Toolbox)安装及使用
  • 【EDA软件】【应用功能子模块网表提供和加载编译方法】
  • 重置 MySQL root 密码
  • 前端面试专栏-主流框架:13.vue3组件通信与生命周期
  • webman 利用tcp 做服务端 对接物联网
  • C# LINQ语法
  • Boss:攻击
  • 【MQTT】常见问题
  • MySQL之视图深度解析
  • 第2章,[标签 Win32] :编写兼容多字节字符集和 Unicode 字符集的 Windows 程序
  • 【DevTools浏览器开发者工具反调试之无限Debugger跳过】
  • SpringBoot高校党务系统
  • PyTorch RNN实战:快速上手教程
  • Python 数据分析与可视化 Day 7 - 可视化整合报告实战
  • Python核心可视化库:Matplotlib与Seaborn深度解析
  • request这个包中,get 这个方法里传入的是params ,post这个方法里传入的是data 和 json。这个区别是什么?
  • pscc系统如何部署,怎么更安全更便捷?
  • Linux 怎么恢复sshd.service
  • 结构体数组与Excel表格:数据库世界的理性与感性