Redis(三):有序集合底层实现

跳跃表

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

跳跃表支持平均 O ( l o g N ) O(logN) O(logN),最坏 O ( N ) O(N) O(N)复杂度的节点查询,所以可以支持顺序性的操作批量去处理节点。

在大部分情况下,跳跃表的效率跟平衡树差不多,但实现起来比平衡树简单。

跳跃表是Redis中有序集合键的底层(也就是ZSet)。

Redis只在两个地方使用了跳跃表,一个是实现有序集合,另一个是在集群节点中用作内部数据结构 ,除此之外,没有任何用途。

跳跃表的实现

Redis的跳跃表由redis.h/zskiplistNode(节点)和redis.h/zskiplist(跳跃表)两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,zskiplistNode结构则用于保存跳跃表节点的相关信息,比如节点数量,以及指向表头结点和表尾结点的指针等等。

跳跃表本质上可以理解是一个有层数的链表
在这里插入图片描述
最左边的Head就是zskiplist结构,该结构包含以下属性

  • header:指向第一个跳跃表的表头结点(是一个傀儡节点,没有什么用)
  • tail:指向最后一个跳跃表的表尾结点(即指向末尾最后的数据节点)
  • Level:记录目前跳跃表内,层数最大的那个节点的层数(不包括表头)
  • length:记录跳跃表的长度,即跳跃表目前包含节点的数量(即数据节点数量)

位于zskiplist结构的右边3个是zskiplistNode结构,该结构包含以下属性

  • 层:结点使用L1、L2、L3等字样标记结点的各个层,L1代表第一层,L2代表第二层,以此类推,每一层就是一个链表,链表里面的结点,拥有两个属性,一个是前进指针,一个是跨度(前进指针就是下一个结点的位置,跨度就是要跨越几个zskiplistNode才能到下一个结点,上图中的栗子跨度都为1,一般都不会这么均匀,越高层的索引链表跨度越大,除了最后一个结点的跨度为0,因为指向了NULL)。
  • 后退指针(BW):BW指向当前结点的上一个结点,当从表尾遍历表头时使用。
  • 分值(score,我这里用VALUE来表示)就是插入数据的键值对的value值。
  • 成员对象(obj,我这里KEY用表示)就是插入数据的键值对的key值。

其实zskiplist是利用了数组来实现跳跃表的同一列指向的score相同,用数组来竖向存储指向同一score的索引,然后数组里面的结点元素再各自形成链表,称为level层索引单向链表。

找元素的过程如下,比如,这里我要找最后一个zskiplistNode结构的key。

此时先找到第一个傀儡结点,然后找到最高层的索引链表(我觉得应该是根据跨度来判断找出最高层的索引列表),傀儡结点找到第一个结点,然后开始对比key值(key值是按升序去排列的),通过对比发现当前结点的score小于要找的score,则在移动到当前链表的下一个结点,发现没有结点,通过当前结点的数组,竖向找下一层索引链表,此时找到下一层索引链表的对应结点的score值是相同的,因为都在同一个zskiplistNode结点内,然后去对比这一层索引链表的下一个结点的key,如果找到,就直接取该结点的value,找不到就依次到下一层索引链表。

跳跃表结点

跳跃表结点由redis.h/zskiplistNode结构定义

typedef struct zskiplistNode{
    //后退指针
 	struct zskiplistNode *backward;
    //分值key
    double score;
    //成员对象value
    robj *obj;
    //层(也是一个结构体,不过是一个数组)
    struct zskiplistLevel{
        //前进指针
        struct zskiplistNode *forward;
        //跨度
		unsigned int span;
    }level[];
}zskiplistNode;

跳跃表结点的level数组可以包含多个元素(不过越前面的结点,包含的元素一般会更多,因为要保证符合跳跃表的要求,即每一层的索引链表必须是下一层的索引链表的子集),一般来说,层越高,找元素的效率就越快。

每次创建一个结点的时候,redis会根据幂次定律(越大的数出现的机率越小)随机生成一个介于1和32之间的值,作为Level数组的大小(也就是这个结点的所拥有的索引高度,即上面有多少个索引链表会可以找到他)

前进指针

每个层里面的元素都有一个指向表尾的指针,用来找到下一个结点(对比key后,发现小了,要往前找,如果碰到NULL,就代表元素不存在)。

跨度

记录两个结点间的距离

  • 两个结点之间的跨度越大,相距就越远
  • 指向NULL的前进指针的跨度都为0

其实跨度的用途是用来计算结点的排位(rank)的,也就是结点的位置,排在最底层的第几个,找到结点的路径遇到的所有层的所有跨度加起来就是结点的排位。

后退指针

每个结点都有后退指针,用来从表尾遍历到表头的。

分值和成员

结点的分值就是存储键值对的value值,是一个double类型的浮点数,结点的顺序是按照value的大小来进行排序的(所以value必须可以解析为浮点型,而且必须唯一,如果重复的话会发生替换)

结点的成员对象是一个指针,指向的对象是一个sds对象(之前提到的SDS字符串),用来储存键值对的key值。

跳跃表

多个跳跃结点就可以形成跳跃表,并且仅仅通过一个zskiplist结构来持有这些跳跃结点。

typedef struct zskiplist{
    //表头结点和表尾结点
    struct skiplistNode *header,*tail;
    //表中结点数量
    unsigned long length;
    //表中层数最大结点的层数(即拥有的索引链表数量)
    int level;
}zskiplist;

header和tail指针分别指向跳跃表的表头和表尾结点,通过这两个指针,程序找到第一个结点和最后一个结点的时间复杂度都为 O ( 1 ) O(1) O(1),并且得到跳跃表的结点个数或者长度,直接返回length即可,时间复杂度也为 O ( 1 ) O(1) O(1)

注意,LEVEL属性是不包括傀儡结点的,傀儡结点里面的数组有很多元素结点没有形成链表,即跨度为0

重点
  • 跳跃表是有序集合的底层实现之一
  • Redis的跳跃表由zskiplist和zskiplistNode组成,前者保存跳跃表信息,后者是跳跃表结点
  • 每个跳跃表结点的层高为1~32
  • 同一个跳跃表中,多个结点可以包含相同的分值(即score可以相同,score必须可以转化为浮点型!),但每个结点的成员对象(一个sds字符串)必须唯一(member必须唯一,出现重复会重置)
  • 跳跃表的结点按照分值进行排序,如果出现分值相同,就按照成员对象的大小进行排序
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值