Redis跳跃表的基本原理和实现代码

作者:袖梨 2022-06-29

本篇文章小编给大家分享一下Redis跳跃表的基本原理和实现代码,文章代码介绍的很详细,小编觉得挺不错的,现在分享给大家供大家参考,有需要的小伙伴们可以来看看。

一、概述

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

如下,对于单个链表来讲,即便链表中存储的数据是有序的,如果我们要向在其中查找某个数据,它只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高,达到了O(n)。

如果我们想要提高其查询效率,可以考虑在链表上构建索引的 方式,每两个节点提取一个节点到上级,我们把抽出来的那一级就叫做索引,如下:

此时,我们假设要查找节点8,我们可以先在索引层遍历,当遍历到索引层中值为7的节点时,发现下一个节点是9,那么要查找的节点肯定在这两个节点之间,我们下降到链表层继续遍历就找到了8这个节点。原来我们在单链表中找到8这个节点要遍历8个节点,而现在有了一级索引后,只需要遍历5个节点。

从上个例子中,我们可以看出,加来一层索引后,查找一个节点需要遍历的节点个数减少了,也就是说查询效率得到了提升,同理我们在一级索引的基础上,在加二级索引。

从图中我们可以看出,查找效率又有了提升,因为在这里例子中我们的数据量很少,当有大量的数据时,我们可以增加多级索引,在查询时,效率可以得到明显的提升。像这种链表增加多种索引的结构,就是跳跃表。

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。

二、跳跃表的实现

Redis的跳跃表由zskiplistNode和zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等,如下,是一个跳跃表的结构:

上图片最左边的是zskiplist结构,该结构包含以下属性: 

header:指向跳跃表的表头节点,通过这个指针程序定位表头节点的时间复杂度就为O(1);

tail:指向跳跃表的表尾节点,通过这个指针程序定位表尾节点的时间复杂度就为O(1);

level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内);通过这个属性可以再O(1)的时间复杂度内获取层高最高的节点的层数

length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)通过这个属性,程序可以再O(1)的时间复杂度内返回跳跃表的长度

上图位于zskiplist结构右方的是四个zskiplistNode结构,该结构包含以下属性: 

层(level):节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离(跨度越大,距离越远)。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。

后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用

分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。

成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员对象

2.1 跳跃表节点的zskiplisNode结构定义

typedef struct zskiplistNode {
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
    // 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;
} zskiplistNode;

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

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

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

节点的后退指针(backward属性)用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点

节点的分值(score属性)是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序;

节点的成员对象(obj属性)是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值

2.2 zskiplist结构的定义

typedef struct zskiplist {
    // 表头节点和表尾节点
    structz skiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;

header和tail指针分别指向跳跃表的表头和表尾节点,通过这两个指针,程序定位表头节点和表尾节点的复杂度为O(1)。

通过使用length属性来记录节点的数量,程序可以在O(1)复杂度内返回跳跃表的长度。

三、结束

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。之所以如此,是因为跳跃表在链表的基础上增加了多级索引以提升查找的效率,但其是一个空间换时间的方案,必然会带来一个问题——索引是占内存的。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略。

相关文章

精彩推荐