聊聊Redis中的跳跃表

news2024/11/24 8:39:56

        Redis 大家项目中应该都用过,哪怕没有分布式锁、幂等校验的一些逻辑使用场景,缓存数据这个大家肯定都用过吧?最简单的key-value格式,直接存储String类型。

        当然,针对越来越复杂的业务场景,后续也可能用到list,hash甚至是zset的存储格式。

        我们知道,Redis的有序集合zset是按照顺序进行排列的,那么这个zset的底层是如何实现的呢?

        JDK中似乎也有一个有序集合的封装类,没错,就是TreeSet!而且TreeSet具有以下特点:

1.没有重复元素,set集合的特性

2.没有索引,直接一个树结构,时间复杂度O(log n)

3.可以将元素按照规则进行排序,可以自己实现定制化排序

4.TreeSet是线程不安全的

5.TreeSet的key不允许为null

        当然,说起Set就不得不说一下它的老搭档Map,TreeSet的搭档自然就是TreeMap了,底层实现自然也在TreeMap那里。

        为了提高查询速度,底层使用的红黑树,元素的增删改查源码也不是很复杂,这里我就贴一段put代码吧,仅供大家参考:

public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
			// 对比
            compare(key, key); 
            root = new Entry<>(key, value, null);
            size = 1;
			// 计数
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // 比较器
        Comparator<? super K> cpr = comparator;
		// 遍历查询设置
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        } else {
			//key为空,直接抛出异常
            if (key == null)
                throw new NullPointerException();
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
		// 计数+红黑树旋转
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

        在JDK代码中,万物皆是对象,通过对对象的封装实现,从而实现具体的算法逻辑。那么Redis呢?纯粹的Key-value啊,哪来的TreeMap结构,当然也可以具体实现,不过Redis没有选择树结构,而是选择了另一种数据结构——跳跃表!

        那么为什么选择跳跃表呢?我看到一个比较有说服力的总结,给大家贴一下:

1、跳表的实现比红黑树更简单:在实现跳表时,代码量相对较少且易于理解和维护。它不需要像红黑树一样进行旋转等复杂操作。

2、跳表的查询效率与红黑树相当:跳表的查询时间复杂度为O(log n),虽然比红黑树略慢,但实际使用起来并没有明显的差别。

3、内存占用更小:跳表相对于平衡树来说,在插入和删除元素时,能够保证相同的时间复杂度,但跳表的内存占用更小。这是因为跳表的节点中只需要保存key值和指向下一个节点的指针,而红黑树需要保存左右子树指针、颜色等额外信息。

4、原子性操作支持更好:在Redis中,跳表可以很方便地实现原子性操作,如插入、删除、查找等。这对于高并发环境下的多线程访问非常有用。

5、性能稳定:虽然红黑树在某些情况下可能会比跳表更快,但是跳表的性能并不会因为特定数据分布或者负载情况而产生大的波动,这使得Redis能够提供更加稳定的性能表现。

接下来,就是揭开跳跃表这个神秘数据结构的面纱了!

        给你一组数据,组装一个数据结构,如何保证这组数据结构是有序的,增删改查速度要快,而且随便给你拿出一个数据,就能知道这个是第多少位?

        似乎有点像LRU?查询最快肯定是加HashMap啊,增删快,那就来个双向链表呗,排序位数的话,这个可能有点小麻烦,单独记录下,还是维护在数据中?似乎都有些局限性。

        咱们先看一看简单的跳跃表,看看跳跃表究竟是如何是吸纳的,嗯,先瞅一瞅度娘给的解释。

增加了向前指针的链表叫作跳表。跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能

        下面是找的一张跳跃表图片,一起来了解下

        首先,一组有序递增数据2,5,9,13,17,22,35,46,54。这组数据如果要进行顺序查询的话,时间复杂度是O(N)。但是我多建立几个层级呢?

        如图,每隔一个数字,我选中,作为第二层的数据,选择到了5,13,33,46这四个数据,感觉还是有点多,于是,我再建立一层,选择13,46,作为第三层。

        这个时候,每一层都是双向链表,同一个数据不同层级使用的是数组维护(就如同图中的三个13)。

        这样,我在查询的17的时候,从第三层开始找到13,然后找到第二层,17小于22,于是还是13,再找第一层,定位到17这个数据,可以看到,对比4次就找到了我们的目标。

        当然,顺序查询的也仅仅需要五次,但是如果数据量比较多呢,几百,几千,几万,甚至几十万呢?可以看出根据跳表这个逻辑结构进行层级查询,类似于数据的二分法,查询时间复杂度是O(logN),当然,为了创建跳跃表这个数据结构,重新建立了数个链表辅助存储,典型的以空间换时间。

        既然谈到了数据结构,肯定少不了我们常挂在嘴边的CRUD(增删改查)!

        查询的话上述已经讲过,时间复杂度是O(logN),类似于平衡二叉树,修改的话肯定也一样啊,查询到直接改了不就得了,没啥多余的动作!

        删除的话也比较简单,如果只有底层节点有的话,比如上图的9,直接删掉就行了,改变一下前后节点的指向。如果多个层级都包含,那么每个层级都要删掉,也即是重复删除一个层级的操作,问题也不大。

        新增的话,这个就有点小头疼了!

         上图是获得一个有序递增链表之后,提取数据,组装而成的跳跃表结构,那么问题来了,如果一个数据组是持续变动的,你知道这个数据要封装几层吗?层数少了,比如只有一层,那几乎就是链表了,没说的,查询的时间复杂度直接来到了O(n)。

        那就多封装几层呗,咱财大气粗,不在乎这点内存!

        

        那每过来一个数据都封装三层,那和一个链表没啥区别啊,直接在第三层找到了目标,一层二层纯粹在那吃干饭,不干活。

        那每隔一个加个层级?

        那又该加几层?层数是多少?3吗,刚开始你知道最终会有多少数据啊.......

       由于是链表结构,直接增加数据的话还是比较简单的,不过这个层级的设置,就要看不同场景中的具体概率算法了,最简单的就是抛硬币算法,决定节点是否加层(或者说提拔一层),因为跳表的添加和删除的节点是不可预测的,很难用算法保证跳表的索引分布始终均匀。虽然抛硬币的方式不能保证绝对均匀,但大体上是趋于均匀的。

        当然最好根据总数,限制下总层数,层级总不能无限上涨,虽然概率极其低。

        在度娘那里,也看到一个级的分配的算法,贴出来给大伙看看,有兴趣的可以深入下。

在级基本的分配过程中,可以观察到,在一般跳表结构中,i-1级链中的元素属于i级链的概率为p。假设有一随机数产生器所产生的数在0到RANDMAX间。则下一次所产生的随机数小于等于CutOff=p*RANDMAX的概率为p。因此,若下一随机数小于等于CutOff,则新元素应在1级链上。现在继续确定新元素是否在2级链上,这由下一个随机数来决定。若新的随机数小于等于CutOff,则该元素也属于2级链。重复这个过程,直到得到一随机数大于CutOff为止。故可以用下面的代码为要插入的元素分配级。

intlev=0;

while(rand()<=CutOff) lev++;

这种方法潜在的缺点是可能为某些元素分配特别大的级,从而导致一些元素的级远远超过log1/pN,其中N为字典中预期的最大数目。为避免这种情况,可以设定一个上限lev。在有N个元素的跳表中,级MaxLevel的最大值为

可以采用此值作为上限。

另一个缺点是即使采用上面所给出的上限,但还可能存在下面的情况,如在插入一个新元素前有三条链,而在插入之后就有了10条链。这时,新插入元素的为9级,尽管在前面插入中没有出现3到8级的元素。也就是说,在此插入前并未插入3,4,⋯,8级元素。既然这些空级没有直接的好处,那么可以把新元素的级调整为3。      

         简单了解了跳跃表了,我们来看下Redis中内置的跳跃表,当然,肯定复杂一些,毕竟有自己的定制化,先上个图!

 redis的跳跃表是由两部分构成。zskiplist和zskiplistNode,zskiplist结构用于保存跳跃表节点的相关信息、指向表头和表尾的指针,层级等。zskiplistNode表是跳跃表节点,有层级、数据、分值、后退指针(backward)BW等。

 1、层:跳跃表的level数组可以包含多个元素,每个元素都包含一个指向其它节点的指针,程序可以根据这些层来加快访问其它节点的速度,一般来说,层的数量越多,访问其它节点的速度就越快

        每次创建个一个跳跃表节点的时候,Redis都会根据幂次定律(越大的数出现的概率越小)随机生成一个介于1-32之间的值作为level数组的大小。也就是高度!

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

3、 跨度:层的跨度(level[i].span)属性,用于记录两个节点之间的距离。

a.两个节点跨度越大,他们相距得就越远;

b.指向null的所有前进指针的跨度都是0,它们没有连接任何节点。

遍历操作主要使用的是前进指针,跨度这个东东主要是用来计算排位的。因为查找只看前后啊,排位才用名次。

 4、后退指针:节点的后退指针用于从表尾到表头方向访问节点,后退指针每次后退至前一个节点。

5、分值和成员:节点的分支(score)是一个double类型的浮点数,跳跃表中的所有节点都是按照分值的大小来排序。节点的成员对象(obj)是一个指针,它指向一个字符串对象,字符串对象则保存着一个SDS值。

        在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的,分值相同的节点将按照成员对象在字典中的大小进行排序,成员对象较小的排在前面(靠近表头)。

        好了,就到这里吧,凡事预则立,不预则废!与君共勉! 

        no sacrifice,no victory!

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

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

相关文章

合宙Air724UG Cat.1模块硬件设计指南--数字语音接口

数字语音接口 简介 数字音频接口DAI&#xff0c;即Digital Audio Interfaces&#xff0c;表示在板级或板间传输数字音频信号的方式。相比于模拟接口&#xff0c;数字音频接口抗干扰能力更强&#xff0c;硬件设计简单&#xff0c;DAI在音频电路设计中得到越来越广泛的应用。 特…

【学习日记2023.6.20】之 分布式事务_CAP定理_BASE理论_微服务集成Seata_Seata的四种事务模式_高可用架构模型

文章目录 1. 分布式事务问题1.1 本地事务1.2 分布式事务1.3 演示分布式事务问题 2. 理论基础2.1 CAP定理2.1.1 一致性2.1.2 可用性2.1.3 分区容错2.1.4 矛盾 2.2 BASE理论2.3 解决分布式事务的思路 3. 初识Seata3.1 Seata的架构3.2 部署TC服务3.2.1 下载3.2.2 解压3.2.3 修改配…

数据库系统概述——第七章 数据库设计(知识点复习+练习题)

&#x1f31f;博主&#xff1a;命运之光 &#x1f984;专栏&#xff1a;离散数学考前复习&#xff08;知识点题&#xff09; &#x1f353;专栏&#xff1a;概率论期末速成&#xff08;一套卷&#xff09; &#x1f433;专栏&#xff1a;数字电路考前复习 &#x1f99a;专栏&am…

p7付费课程笔记:jvm基础知识、字节码、类加载器

编程语言 演化&#xff1a; 机器语言->编程语言->高级语言&#xff08;java&#xff0c;c,Go,Rust等&#xff09; 面向过程–面向对象-面向函数 java是一种面向对象、静态类型、编译执行&#xff0c;有VM&#xff08;虚拟机&#xff09;/GC和运行时、跨平台的高级语言…

第二章 视觉感知与视觉通道(复习)

大纲 视觉感知 认知 视觉通道 色彩* 可视化致力于外部认知&#xff0c;也就是说&#xff0c;怎样利用大脑以外的资源来增强大脑本身的认知能力。 感知是指客观事物通过人的感觉器官在人脑中形成的直接反映 感觉器官&#xff1a;眼、耳、口、鼻、神经末梢 视觉感知就是客观事物通…

世界史上五个横跨亚欧非三大洲的超强帝国

古代地中海和西亚地区文明出现的很早&#xff0c;经济文化社会都比较先进&#xff0c;其中古埃及早在四千多年前就建立了庞大的帝国&#xff0c;给世人留下了不朽的金字塔&#xff1b;两河流域、希腊半岛也很早就出现了城邦制的国家&#xff0c;也创造了灿烂的文明。同时&#…

架构设计我们要注意什么?

这几天我正在做一个新项目的架构设计&#xff0c;关于动态流程引擎平台的搭建&#xff0c;涉及到了系统架构的设计&#xff0c;里面涉及了方方面面&#xff0c;所以就想着结合自己的实际经验&#xff0c;遇到的问题&#xff0c;以及自己的理解&#xff0c;为大家做一个简单的分…

损失函数:IoU、GIoU、DIoU、CIoU、EIoU、alpha IoU、SIoU、WIoU超详细精讲及Pytorch实现

前言 损失函数是用来评价模型的预测值和真实值不一样的程度&#xff0c;损失函数越小&#xff0c;通常模型的性能越好。不同的模型用的损失函数一般也不一样。 损失函数的使用主要是在模型的训练阶段&#xff0c;如果我们想让预测值无限接近于真实值&#xff0c;就需要将损…

献给蓝初小白系列(二)——Liunx应急响应

1、Linux被入侵的症状​​ ​​https://blog.csdn.net/weixin_52351575/article/details/131221720​​ 2、Linux应急措施 顺序是&#xff1a;隔离主机--->阻断通信--->清除病毒--->可疑用户--->启动项和服务--->文件与后门--->杀毒、重装系统、恢复数据 …

python代码加密方案

为何要对代码加密&#xff1f; python的解释特性是将py编译为独有的二进制编码pyc 文件&#xff0c;然后对pyc中的指令进行解释执行&#xff0c;但是pyc的反编译却非常简单&#xff0c;可直接反编译为源码&#xff0c;当需要将产品发布到外部环境的时候&#xff0c;源码的保护尤…

Guitar Pro是什么软件 Guitar Pro有什么用

相信玩吉他的朋友多多少少都听说过Guitar Pro这款软件&#xff0c;那大家知道Guitar Pro是什么软件&#xff1f;Guitar Pro有什么用呢&#xff1f;今天小编就和大家分享一下关于Guitar Pro这款吉他软件的相关内容。 一、Guitar Pro是什么软件 简单说Guitar Pro是一款吉他谱软…

Vue实现元素沿着坐标数组移动,超出窗口视图时页面跟随元素滚动

一、实现元素沿着坐标数组移动 现在想要实现船沿着下图中的每个河岸移动。 实现思路&#xff1a; 1、将所有河岸的位置以 [{x: 1, y: 2}, {x: 4, y: 4}, …] 的形式保存在数组中。 data() {return {coordinateArr: [{ x: 54, y: 16 }, { x: 15, y: 31 }, { x: 51, y: 69 }…

leetcode77. 组合(回溯算法-java)

组合 leetcode77. 组合题目描述解题思路代码演示 递归专题 leetcode77. 组合 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/combinations 题目描述 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个…

量化交易:止盈策略与回测

我们买基金或股票的时候通常用最简单的策略进行决策&#xff1a;低买高卖&#xff0c;跌的多了就加仓拉低持有成本&#xff0c;达到收益率就卖出。 那么如何用代码表示这个策略呢&#xff1f;首先定义交易信号则是&#xff1a;0.5%时买入&#xff0c;目标止盈线是1.5%&#xf…

Java官方笔记12异常

Exception Definition: An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the programs instructions. the checked exception 比如&#xff0c;java.io.FileNotFoundException the error 比如&#xff0c;java.i…

Flink流批一体计算(2):Flink关键特性

目录 流式处理 丰富的状态管理 流处理 自定义时间流处理 有状态流处理 通过状态快照实现的容错 流式处理 在自然环境中&#xff0c;数据的产生原本就是流式的。无论是来自 Web 服务器的事件数据&#xff0c;证券交易所的交易数据&#xff0c;还是来自工厂车间机器上的…

优先级队列建立小根堆来解决前K个高频元素(TOP K问题)

目录 场景一&#xff1a;解决前K个高频元素需要解决如下几个问题&#xff1a; 优先级队列PriorityQueue 堆的定义 题目链接 场景二&#xff1a;亿万级数据取前TOP K / 后TOP K 数据 场景一&#xff1a;解决前K个高频元素需要解决如下几个问题&#xff1a; 1.记录每一个元…

【C++】4.工具:读取ini配置信息

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍读取ini配置信息。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路&…

医院PACS系统的发展历史

PACS全称Picture Archivingand Communication Systems。它是应用在医院影像科室的系统&#xff0c;主要的任务就是把日常产生的各种医学影像&#xff08;包括核磁&#xff0c;CT&#xff0c;超声&#xff0c;X光机&#xff0c;红外仪、显微仪等设备产生的图像&#xff09;通过各…

【工程应用八】终极的基于形状匹配方案解决(小模型+预生成模型+无效边缘去除+多尺度+各项异性+最小组件尺寸)...

我估摸着这个应该是关于形状匹配或者模版匹配的最后一篇文章了&#xff0c;其实大概是2个多月前这些东西都已经弄完了&#xff0c;只是一直静不下来心整理文章&#xff0c;提醒一点&#xff0c;这篇文章后续可能会有多次修改(但不会重新发文章&#xff0c;而是在后台直接修改或…