Redis源码学习:跳表(Skip List)的工作原理详解

什么是跳表

跳表(Skip List),首先它是链表,是一种随机化的数据结构,Redis 使用跳表作为有序集合(Sorted Set)的底层实现之一。跳表能够提供高效的插入、删除、查找操作。本文通过阅读源码来分析跳表的工作原理。

跳表的设计思想

跳表的设计思想是通过多级链表来加速查找操作。在一个简单的链表中,查找元素的时间复杂度是 O(n),而在跳表中,通过引入多级索引,查找操作的平均时间复杂度可以降到 O(log n)。

跳表的基本结构如下图所示:

Level 4:        1---------------------->7
Level 3:        1-------->4------------>7
Level 2:        1-------->4------>6---->7
Level 1:  0---->1---->2-->4-->5-->6-->7-->8

每个节点以一定的概率提升到更高一级,形成多个层级的链表。最高层级的链表包含所有节点的索引,而底层链表包含所有节点。

跳表的结构

在 Redis 的源码中,跳表的数据结构定义在 server.h 文件中,主要由以下几个结构体组成:

  • zskiplistNode:跳表节点。
  • zskiplist:跳表。
// 跳表节点
typedef struct zskiplistNode {double score;                       // 节点的分数,排序、查找使用sds ele;                            // 节点的值struct zskiplistNode *backward;     // 后退指针,指向前一个节点struct zskiplistLevel {struct zskiplistNode *forward;  // 前进指针,指向后一个节点unsigned int span;              // 跨度} level[];                          // 层级数组
} zskiplistNode;// 跳表
typedef struct zskiplist {struct zskiplistNode *header, *tail; // 头节点和尾节点unsigned long length;                // 跳表长度int level;                           // 当前最大层级,默认1
} zskiplist;

跳表的操作

接下来,我们来看一下 Redis 中对跳表的主要操作:插入、删除和查找。具体实现代码在 t_zset.c 文件中:

创建跳表节点

创建一个新的跳表节点:

zskiplistNode* zslCreateNode(int level, double score, sds ele) {// 为节点分配内存,level 决定节点具有的层数zskiplistNode *zn = zmalloc(sizeof(*zn) + level * sizeof(struct zskiplistLevel));zn->score = score;  // 节点的分数zn->ele = ele;      // 节点的值return zn;
}

创建跳表

创建一个新的跳表:

zskiplist* zslCreate(void) {int j;zskiplist *zsl;// 分配跳表的内存zsl = zmalloc(sizeof(*zsl));zsl->level = 1;  // 初始层级为 1zsl->length = 0; // 初始长度为 0// 创建头节点,最大层数为 ZSKIPLIST_MAXLEVELzsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL, 0, NULL);for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {zsl->header->level[j].forward = NULL;zsl->header->level[j].span = 0;}zsl->header->backward = NULL;zsl->tail = NULL;return zsl;
}

插入

插入操作的核心在于找到新节点插入的位置,并更新相关的指针:

zskiplistNode* zslInsert(zskiplist *zsl, double score, sds ele) {zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;unsigned int rank[ZSKIPLIST_MAXLEVEL];int i, level;x = zsl->header;  // 从头节点开始for (i = zsl->level-1; i >= 0; i--) {  // 从最高层往下遍历while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele, ele) < 0))) {  // 查找插入位置rank[i] += x->level[i].span;x = x->level[i].forward;}update[i] = x;  // 记录每层的前驱节点}level = zslRandomLevel();  // 随机生成新节点的层数if (level > zsl->level) {  // 如果新节点层数超过当前最大层数for (i = zsl->level; i < level; i++) {rank[i] = 0;update[i] = zsl->header;update[i]->level[i].span = zsl->length;}zsl->level = level;  // 更新跳表的层数}x = zmalloc(sizeof(*x)+level*sizeof(struct zskiplistLevel));  // 分配新节点内存x->score = score;x->ele = sdsdup(ele);for (i = 0; i < level; i++) {  // 插入新节点,并更新相关指针和跨度x->level[i].forward = update[i]->level[i].forward;update[i]->level[i].forward = x;x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);update[i]->level[i].span = (rank[0] - rank[i]) + 1;}for (i = level; i < zsl->level; i++) {update[i]->level[i].span++;}x->backward = (update[0] == zsl->header) ? NULL : update[0];  // 更新 backward 指针if (x->level[0].forward)x->level[0].forward->backward = x;elsezsl->tail = x;  // 如果新节点是最后一个节点,更新尾节点指针zsl->length++;  // 更新跳表的长度return x;
}

这段代码做了以下几个关键步骤:

  1. 遍历各层,找到新节点的插入位置。
  2. 随机确定新节点的层数,并更新跳表的层数。
  3. 插入新节点,并更新相关指针和跨度。

随机层级生成

在插入操作中有一个随机层级的生成操作,使用的随机函数zslRandomLevel

int zslRandomLevel(void) {// 初始层数是1int level = 1;// 以 ZSKIPLIST_P 的概率提升层级,随机层数的值是0.25while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))level += 1;// ZSKIPLIST_MAXLEVEL 最大层数是64return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

删除

删除操作的核心在于找到要删除的节点,并更新相关的指针。

int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;int i;x = zsl->header;  // 从头节点开始for (i = zsl->level-1; i >= 0; i--) {  // 从最高层往下遍历while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele, ele) < 0))) {  // 查找要删除的节点位置x = x->level[i].forward;}update[i] = x;  // 记录每层的前驱节点}x = x->level[0].forward;  // 指向要删除的节点if (x && score == x->score && sdscmp(x->ele, ele) == 0) {  // 确认节点存在zslDeleteNode(zsl, x, update);  // 删除节点,并更新指针if (!node) zfree(x->ele);  // 释放节点内存zfree(x);return 1;  // 删除成功}return 0;  // 节点不存在,删除失败
}

在删除操作中:

  1. 遍历各层,找到要删除节点的位置。
  2. 删除节点,并更新相关指针和跨度。

查找

查找操作相对简单,核心在于从高层到低层逐层遍历,直到找到目标节点或确认目标节点不存在。

unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {zskiplistNode *x;unsigned long rank = 0;int i;x = zsl->header;  // 从头节点开始for (i = zsl->level-1; i >= 0; i--) {  // 从最高层往下遍历while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele, ele) < 0))) {  // 查找目标节点rank += x->level[i].span;x = x->level[i].forward;}if (x->level[i].forward && score == x->level[i].forward->score && sdscmp(x->level[i].forward->ele, ele) == 0) {  // 找到目标节点rank += x->level[i].span;return rank;  // 返回目标节点的排名}}return 0;  // 目标节点不存在
}

在查找操作中:

  1. 从最高层开始,逐层向前移动,直到找到目标节点或确认其不存在。
  2. 返回目标节点的排名(或返回 0 表示不存在)。

在比较结点时,相应地有两个判断条件:

  1. 当查找到的结点保存的元素权重,比要查找的权重小时,跳表就会继续访问该层上的下一个结点。
  2. 当查找到的结点保存的元素权重,等于要查找的权重时,跳表会再检查该结点保存的 SDS 类型数据,是否比要查找的 SDS 数据小。如果结点数据小于要查找的数据时,跳表仍然会继续访问该层上的下一个结点。

总结

Redis 的跳表通过多级索引结构,实现了高效的插入、删除和查找操作。希望这篇文章能够帮助你更好地理解跳表的工作原理和实现细节。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/857609.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python对象转字典对象及序列化

python对象转字典 1、为什么要对python对象转换为字典 python字典与json对象结构相似&#xff0c;json数据结构是最常用的数据结构 2、python对象转字典及序列化的核心点在哪 python对象有很多内置属性并不是我们所需要的python字典对象在序列化的时候&#xff0c;时间字段…

grafana 通过自定义API获取数据

一、安装插件 安装infinity插件 二、配置数据源 三、配置图表 1、数据 这边提供一个go的demo package mainimport ("math/rand""net/http""time""github.com/gin-gonic/gin" )func main() {router : gin.Default()rand.Seed(time.…

【SQL】UNION 与 UNION ALL 的区别

在 SQL 中&#xff0c;UNION 和 UNION ALL 都用于将两个或多个结果集合并为一个结果集&#xff0c;但它们在处理重复数据方面有显著区别。以下是它们的详细区别&#xff1a; 1. UNION UNION 操作符用于合并两个或多个 SELECT 语句的结果集&#xff0c;并自动去除结果集中重复…

c# 学习笔记 PropertyChangedEventHandler、 =>、DependencyObject、DataContext

在C#中&#xff0c;PropertyChangedEventHandler 和 PropertyChanged 常常与 INotifyPropertyChanged 接口一起使用&#xff0c;这是实现数据绑定和通知机制的关键部分&#xff0c;尤其在WPF (Windows Presentation Foundation) 或其他支持数据绑定的UI框架中。 PropertyChang…

怎样去掉卷子上的答案并打印

当面对试卷答案的问题时&#xff0c;一个高效而简单的方法是利用图片编辑软件中的“消除笔”功能。这种方法要求我们首先将试卷拍摄成照片&#xff0c;然后利用该功能轻松擦除答案。尽管这一方法可能需要些许时间和耐心&#xff0c;但它确实为我们提供了一个可行的解决途径。 然…

【2024.6.22】今日科技时事:科技前沿大事件

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

如何避免vue的url中使用hash符号?

目录 1. 安装 Vue Router 2. 配置 Vue Router 使用 history 模式 3. 更新 main.js 4. 配置服务器以支持 history 模式&#xff08;此处需要仔细测试&#xff09; a. Nginx 配置 b. Apache 配置 5. 部署并测试 总结 在 Vue.js 项目中&#xff0c;避免 URL 中出现 # 符号的…

docker-compose功能、操作

文章目录 前言主要功能基本用法 前言 docker-compose 是一个用于定义和运行多容器 Docker 应用的工具。它使用一个 YAML 文件&#xff08;通常命名为 docker-compose.yml&#xff09;来配置应用的服务、网络和卷等属性。通过 docker-compose&#xff0c;你可以利用一个单一的命…

YAML 入门教程

YAML 是 "YAML Aint a Markup Language"&#xff08;YAML 不是一种标记语言&#xff09;的递归缩写&#xff0c;是一种人类可读的完整的数据序列化语言。YAML 的意思其实是&#xff1a;"Yet Another Markup Language"&#xff08;仍是一种标记语言&#xf…

宝塔部署GeoServer教程

前期准备&#xff1a;下载geoserver 直接用我上传的geoserver或者是去官网下https://geoserver.org/release/stable/ 压缩包的geoserver版本是&#xff1a;2.25.1 jdk要求版本是&#xff1a;jdk11以上 tomcat版本&#xff1a;可选8或者9&#xff0c;建议用9 windows选择Window…

linux./xxx.py :Command not found

从windows传入linux系统中的py文件&#xff0c;在运行时出现了如标题所示的错误 第一行#!/bin/python3 但是却无法使用./xxx.py运行&#xff0c;通过一番调试&#xff0c;发现 windows的换行符与linux的换行符不一致导致了错误的发生 如何解决这个问题&#xff0c;使用dos2unix…

视频监控统一管理平台LntonCVS安防视频监控系统视频汇聚方案

LntonCVS平台最初被设计为一个以视频汇聚为核心的平台。那么&#xff0c;什么是视频汇聚平台&#xff0c;以及它是如何处理视频资源的呢&#xff1f;简单来说&#xff0c;视频汇聚平台能够从不同的视频源&#xff08;如直播和点播&#xff09;收集、整合和展示视频内容。以下是…

春天,快速恢复能量的10件小事(不妨试试)

春天快速恢复能量的十件小事&#xff0c;不妨试试随着天气回暖&#xff0c;万物都在恢复生机。 只是很多朋友感慨自己似乎总有些能量不足&#xff0c;没干什么重活累活&#xff0c;但觉得浑身疲乏&#xff0c;精神状态很低迷&#xff0c;不仅容易走神&#xff0c;而且记忆力也在…

Docker可视化web工具

docker run --restart always --name docker.ui -d -v /var/run/docker.sock:/var/run/docker.sock -p 8989:8999 joinsunsoft/docker.ui #--restart always&#xff1a;重启策略&#xff0c;只要关闭就会重启 http://192.168.10.51:8989 账号&#xff1a;ginghan 密码&#xf…

IOS Swift 从入门到精通: 函数,参数和异常

文章目录 编写函数接受参数返回值参数标签省略参数标签默认参数可变参数函数编写抛出函数运行异常函数输入输出参数总结 编写函数 函数让我们可以重复使用代码&#xff0c;这意味着我们可以编写一个函数来做一些有趣的事情&#xff0c;然后从很多地方运行该函数。重复代码通常…

Java匿名类

Java 匿名类是一种特殊的内部类&#xff0c;它没有名字&#xff0c;并且通常用来简化代码实现&#xff0c;尤其是在实现接口或者抽象类的实例时。匿名类可以在实例化时定义其行为&#xff0c;而不需要创建单独的类文件。 匿名类的特点 没有名字&#xff1a;匿名类是没有名字的…

写代码必用字体

下载链接 字体下载链接 使用情况/截图 软件&#xff1a;DEV-CPP 系统&#xff1a;Win10专业版 自带判等、大于、小于等符号的专属字体

计算机组成原理网课笔记2

存储系统基本概念 CPU&#xff1a;运算器控制器。​ 作为计算机系统的运算和控制核心&#xff0c;是信息处理、程序运行的最终执行单元。 ​ 主存储器的基本组成 半导体元件的原理 在电容上面的金属板加一个5V的高电平&#xff0c;产生电压差&#xff0c;电容里面的电荷就会开…

计算机系统基础实训五—CacheLab实验

实验目的与要求 1、让学生更好地应用程序性能的优化方法&#xff1b; 2、让学生更好地理解存储器层次结构在程序运行过程中所起的重要作用&#xff1b; 3、让学生更好地理解高速缓存对程序性能的影响&#xff1b; 实验原理与内容 本实验将帮助您了解缓存对C程序性能的影响…

带你学习Mybatis之MappedStatement

mybatis之MappedStatement MappedStatement是保存mapper.xml的一个节点(select|insert|delete|update)&#xff0c;包括许多配置的sql、sql的id、缓存信息、resultMap、parameterType、resultType、languageDriver等重要配置内容 public final class MappedStatement { privat…