Redis第3讲——跳跃表详解

一、什么是跳跃表

跳跃表(skiplist)是一种随机化的数据结构,由William Pugh在论文《Skip lists: a probabilistic alternative to balanced trees》中提出。它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。

跳跃表支持O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点,在大部分情况下,跳跃表的效率可以和红黑树、AVL树不相上下,但跳表原理更加简单、实现起来也更简单直观。

在Redis中,跳跃表是有序集合(zSet)数据类型的实现之一,也在集群节点中用作内部数据结构,除此之外,跳跃表在Redis里面没有其它用途。

二、举个例子

是不是有点抽象?那么下面我们举个有序链表的例子对比一下,就更容易理解了。

我们先来看下有序链表:

91f9d0dac8ef44318d523c7aabe9fa99.png

在这个链表中,如果我们想要查找一个数,需要从头结点开始向后依次遍历和比对,直到查到为止,那么它的时间复杂度就是O(N)。

当我们想要在这个链表插入一个数,过程和查找了类似,也是从头到尾遍历,然后在合适为止再插入,时间复杂度也是O(N)。

那有什么好办法呢?如果我们将链表中每两个节点建立第一级索引,是否可以提升效率呢,如下图:

4f9f571ee7fa4664ae0de0ab818afa6a.png

有了索引后,如果我们查询10元素,我们先从一级索引5、8、17、29中查找:

  • 先和5比较,发现10比5大,继续向后找。
  • 8比10小,继续向后找。
  • 发现8的next指针指向17,比10大,然后下降到0层。
  • 此时第0层的next指针指向10,也就是我们要找的元素。

可以看到,同样是查找10,有序链表需要比较(2,5,7,8,10)五个元素,而建了一层索引后,仅需比较(5,8,17,10)四个元素。

有了上面经验,我们在此基础上继续建立二级...

5aac12a33d5e4512a6cf1eb828b9578a.png

  • 首先和8进行比较,发现比10小,向后查找。
  • 此时next指向NULL,下降一层。
  • 此时next指针指向17,比10大,下降一层。
  • 此时next指针指向10,正是我们要找的元素。

此时只需比较(5、8、10)。

综上所述,通过将有序集合的部分节点分层,从最上层节点依次开始向后查找,如果本层的next节点大于查找值或指向NULL,则从本节开始下降一层继续向后查找,如果找到则返回,反之返回NULL。

因为我们的链表不够大,查找的元素也比较靠前,所以速度上的感知可能没那么大,但是如果是在成千上万个节点、甚至数十万、百万个节点中遍历,那么这样的数据结构就能大大提高效率,这就是跳跃表的思想。

三、跳跃表的实现

Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量、指向表头结点和表尾系欸但的指针等,下面来分别介绍一下。

3.1 zskiplit结构

保存跳跃表节点的相关信息,比如节点的数量、指向表头结点和表尾系欸但的指针等,定义如下:

/** 跳跃表链表结构*/
typedef struct zskiplist {// 表头节点和表尾节点struct zskiplistNode *header, *tail;// 表中节点的数量unsigned long length;// 表中层数最大的节点的层数int level;
} zskiplist;
  • header:指向跳跃表的表头结点。
  • tail:指向跳跃表的表尾结点。
  • length:跳跃表长度,表示第0层除头节点以外的所有结点总数。
  • level:跳跃表高度,除头节点外,其它节点层数最高的即为跳跃表高度。

ps:表头节点和其它节点的构造是一样的:表头节点也有后退指针、分值和成员对象,不过表头节点的这些属性都不会被用到。

3.2 zskiplistNode结构

表示跳跃表节点,定义如下:

typedef struct zskiplistNode {// 成员对象robj *obj;// 分值double score;// 后退指针struct zskiplistNode *backward;// 层struct zskiplistLevel {// 前进指针struct zskiplistNode *forward;// 跨度unsigned int span;} level[];
} zskiplistNode;
  • 成员对象(obj):节点的成员对象,指向一个字符串对象,而字符串对象则保存着一个SDS值。

  • 分值(score):double类型浮点数,用户存储有序链表节点的分值,跳跃表中所有节点都按分值从小到达排序。

  • 后退(backward)指针:后退指针,用于从表尾向表头访问,与可以一次跳过多个前进指针不同,因为每个节点只有一个后退指针,因此一次只能后退一步。

  • 层(level):每次创建一个新跳跃节点时,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个1~32的值作为level数组的大小,这个大小就是层的“高度”,一般来说,层的数量越多,访问其它节点的速度就越快。这个level数组中的每项元素包含以下两个元素:

    • forward:每个层都有一个指向表尾方向的前进指针(level[i].forward属性),用于从表头向表尾方向访问节点,尾节点的forward指向NULL。

    • span:层的跨度(level[i].span属性),用于记录两个节点之间的距离,两个节点之间的跨度越大,说明它们距离的越远,指向NULL的所有前进指针的跨度都为0,因为他没有连向任何节点。

如果能仔细看到这的话,看一眼图,会让你茅塞顿开:最左边是zskiplist结构,右边四个是zskiplistNode结构,其中第一个为头节点(header)。

6022cdf70c86450d8be8be55c7c9387f.png

3.3 随机高度的实现

每一个新插入的节点,都会调用一个随机算法给它分配一个随机层数,定义如下:

#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
#define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */
/* Returns a random level for the new skiplist node we are going to create.* 返回一个随机值,用作新跳跃表节点的层数。* 返回值介乎 1 和 ZSKIPLIST_MAXLEVEL 之间(包含 ZSKIPLIST_MAXLEVEL),* 根据随机算法所使用的幂次定律,越大的值生成的几率越小。* T = O(N)*/
int zslRandomLevel(void) {int level = 1;//0xFFFF十六进制对应65535十进制while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))level += 1;return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

redis通过zslRandomLevel函数随机生成一个1~32的值作为新建节点的高度,值越大出现的概率越低,节点高度确定后就不会再修改。解释一下上述代码:

  • level初始值为1,进入循环。
  • 如果一个0~65535(含)之间的随机数小于ZSKIPLIST_P(0.25) * 65535,则level加一,反之跳出循环。
  • 最后一个三元运算,取level和ZSKIPLIST_MAXLEVEL(32)的最小值返回。

所以,我们可以看到,redis跳跃表默认允许最大的层数就是32,也就是ZSKIPLIST_MAXLEVEL。

四、相关API

源码就不介绍了,这里给大家提供一个API,感兴趣的可以根据下图去学习相关的源码:

b27ab3e499384581bfeef09d3b522395.png

五、为什么zset数据类型既支持高效的范围查询且能以O(1)的时间复杂度获取元素权重值?

高效的范围查询是因为它的核心数据结构是跳跃表,而能以O(1)的时间复杂度获取元素权重值是因为同时采用了哈希表进行索引。

ps:我们直到zset数据类型的编码是skiplist和ziplist(redis 7.0 istpack),而skiplist的底层采用了zset结构,千万别搞混了!

typedef struct zset
{dict *dict;zskiplist *zsl;
}zset;

以上是zset结构,包含了两个成员,哈希表dict和调表zsl。

  • dict存储member->score之间的映射关系,所以ZSCORE的时间复杂度为O(1)。
  • 从上述跳跃表的介绍中也不难理解,skiplist是一个【有序链表+多层索引】的结构,每一层级的索引包含了指向其它链表中其它节点的指针,通过这些指针可以跳过一些不需要的节点,从而快速定位到目标元素,查询效率的时间复杂度为O(log N),所以它的查询效率也很高。

  End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

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

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

相关文章

Zabbix 监控介绍

1、功能概述 通常所说的监控&#xff0c;会模糊地包含以上下个细分领域的内容&#xff1a; 应用性能监控&#xff08;Application Performance Monitoring&#xff09;业务交易监控&#xff08;Business Transaction Monitoring&#xff09;网络性能监控&#xff08;Network …

华为云CES监控与飞书通知

华为云负载均衡连接数监控与飞书通知 在云服务的日常运维中&#xff0c;持续监控资源状态是保障系统稳定性的关键步骤之一。本文通过一个实际案例展示了如何使用华为云的Go SDK获取负载均衡器的连接数&#xff0c;并通过飞书Webhook发送通知到团队群组&#xff0c;以便运维人员…

福利来袭,.NET Core开发5大案例,30w字PDF文档大放送!!!

千里之行&#xff0c;始于足下&#xff0c;若想提高软件编程能力&#xff0c;最最重要的是实践&#xff0c;所谓纸上得来终觉浅&#xff0c;绝知此事要躬行。根据相关【艾宾浩斯遗忘曲线】研究表明&#xff0c;如果不动手实践&#xff0c;记住的东西会很快忘记。 为了便于大家查…

C#设计模式之观察者模式

前言 观察者&#xff08;Observer)模式也称发布-订阅&#xff08;Publish-Subscribe&#xff09;模式&#xff0c;定义了对象间一种一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。 观察者模式的图解如下所示…

小程序测试和APP测试的区别

今天看了一下关于如何测试小程序的教学视频&#xff0c;里面讨论了一个很经典的面试题&#xff1a;小程序测试和APP测试的区别&#xff0c;包括在之前的面试过程中也确实是遇到过这个问题&#xff0c;所以这次打算把它记录下来&#xff0c;也算是知识巩固了。 首先从测试的内容…

android7以上 代码安装APK

一、所需权限 <!--请求安装APK的权限--> <uses-permission android:name"android.permission.REQUEST_INSTALL_PACKAGES" /> <!--写如外部存储的权限--> <uses-permission android:name"android.permission.WRITE_EXTERNAL_STORAGE"…

【DevOps-03】Build阶段-Maven安装配置

一、简要说明 下载安装JDK8下载安装Maven二、复制准备一台虚拟机 1、VM虚拟复制克隆一台机器 2、启动刚克隆的虚拟机,修改IP地址 刚刚克隆的虚拟机 ,IP地址和原虚拟的IP地址是一样的,需要修改克隆后的虚拟机IP地址,以免IP地址冲突。 # 编辑修改IP地址 $ vi /etc/sysconfig…

内存管理的概念-第四十一天

目录 前言 内存空间的分配与回收 内存空间的扩展 地址转换 存储保护 上下限寄存器 重定位寄存器和界地址寄存器 本节思维导图 前言 操作系统作为系统资源的管理者&#xff0c;当然也需要对内存进行管理&#xff0c;要管理什么呢&#xff1f; 操作系统复杂内存空间的分…

Lazada商品详情API(lazada.item_get)进行商品的实时更新

一、引言 在数字时代&#xff0c;电商平台如Lazada成为了商品交易的重要场所。为了保持竞争力&#xff0c;实时更新商品信息变得至关重要。Lazada提供的商品详情API&#xff08;lazada.item_get&#xff09;为开发者提供了一个高效的方式来获取并更新商品数据。本文将深入探讨…

SpringBoot全局Controller返回值格式统一处理

一、Controller返回值格式统一 1、WebResult类 在 Controller对外提供服务的时候&#xff0c;我们都需要统一返回值格式。一般定义一个 WebResult类。 统一返回值&#xff08;WebResult类&#xff09;格式如下&#xff1a; {"success": true,"code": 2…

express+mongoDB开发入门教程之mongoose使用讲解

系列文章 node.js express框架开发入门教程 expressmongoDB开发入门教程之mongoDB安装expressmongoDB开发入门教程之mongoose使用讲解 文章目录 系列文章前言一、Mongoose是什么&#xff1f;二、Mongoose安装三、Mongoose在express项目中使用步骤一、连接mongoDB数据库步骤二、…

模拟器怎么代理IP?代理IP对手机设置模拟器有哪些影响?

一、代理IP的基本概念和作用流冠代理IP是一种网络服务&#xff0c;可以帮助用户隐藏自己的真实IP地址&#xff0c;通过代理服务器进行网络请求&#xff0c;从而保护用户的隐私和安全。在模拟器中&#xff0c;代理IP的作用也是如此&#xff0c;可以帮助模拟器隐藏真实的IP地址&a…

javascript 常见工具函数(一)

1.将JSON数据根据相同值&#xff0c;进行归类划分&#xff1a; var arr [{ time: "1", img: "22222" }, { time: "2", img: "555" }, { time: "1", img: "888888" }, { time: "2", img: "4444&q…

MySQL Too many connections报错

MySQL 时不时出现Too many connections报错&#xff0c;重启MySQL就好了 但是过段时间又出现 一、解决方案&#xff1a; 1.修改mysql最大连接数 set global max_connections500; 以上是修改立即生效的&#xff0c;重启MySQL就会还原回去 在MySQL配置文件修改 max_connection…

力扣刷题-二叉树-二叉搜索树中的搜索

700 二叉搜索树中的搜索 给定二叉搜索树&#xff08;BST&#xff09;的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在&#xff0c;则返回 NULL。 例如&#xff0c; 在上述示例中&#xff0c;如果要找的值是 5&#x…

UDP单播

CMakeLists.txt文件中添加如下行&#xff1a; link_libraries(ws2_32) 1.发送端 #include <iostream> #include <winsock2.h> #include <cstdio>#pragma comment(lib, "Ws2_32.lib") // Link with ws2_32.libint main() {1.Initialize winsock…

JS 手写 new 函数

工作中我们经常会用到 new 关键字&#xff0c;new 一个构造函数生成一个实例对象&#xff0c;那么new的过程中发生了什么呢&#xff0c;我们今天梳理下 创建一个对象对象原型继承绑定函数this返回对象 先创建一个构造函数&#xff0c;原型上添加一个方法 let Foo function (n…

03、Kafka ------ CMAK(Kafka 图形界面管理工具) 下载、安装、启动

目录 CMAK&#xff08;Kafka 图形界面管理工具&#xff09;下载安装启动打开 cmak 图形界面 CMAK&#xff08;Kafka 图形界面管理工具&#xff09; Kafka本身并没有提供Web管理工具&#xff0c;而是推荐使用bin目录下各种工具命令来管理Kafka&#xff0c; 这些工具命令其实用起…

vue3中标签form插件

想写一个系统&#xff0c;对八字进行标注&#xff0c;比如格局&#xff0c;有些八字就有很多格局&#xff0c;于是就想着使用el-tag但是&#xff0c;form表单中如何处理呢&#xff1f; 这个时候&#xff0c;就需要自己写一个,modelValue是表单的默认属性 <template><…

以 Serverfull 方式运行无服务器服务

当前 IT 架构中最流行的用例是从 Serverfull 转向 Serverless 设计。在某些情况下&#xff0c;我们可能需要以 Serverfull 方式设计服务或迁移到 Serverfull 作为运营成本的一部分。 在本文中&#xff0c;我们将展示如何将 Kumologica flow 作为 Docker 容器运行。通常&#x…