redis 系列——5、跳跃表

概述

上篇博客我简单介绍了 redis 字典的实现原理,本篇博客我打算整理 redis 跳跃表实现原理。关于抽象概念跳跃表相关知识可以 点击这里 查看我之前的博客。


跳跃表

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

跳跃表查询的平均复杂度为 O(log n),最坏复杂度为 O(n)。在大多数情况下,它的效率和平衡树差不多,但技术难度上跳跃表相比平衡树简单许多。目前绝大多数程序使用跳跃表代替平衡树。

redis 使用跳跃表作为有序集合键(zSet)的实现方式之一:如果一个有序集合包含的元素数量比较多,或者说集合元素都是比较长的字符串时,redis 使用跳跃表作为有序集合键的实现原理。

除了有序集合键外,redis 还在集群节点中使用跳跃表作为内部数据结构,其中 redis 仅仅在这两块内容用到跳跃表。


zSkipListNode

redis 跳跃表是由头文件 redis.h 中的 zSkipListNodezSkipList 两结构实现的:

  • zSkipList:记录跳跃表本身,通过它快速访问和跳跃表相关的常用属性
  • zSkipListNode:跳跃表中节点元素,通过它记录所有节点值

下面我们首先来看 zSkipListNode 的结构:

typedef struct zskiplistNode {
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
    // 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;
} zskiplistNode;
  • level:跳跃表的层级,其中每个数组元素包含两个属性:指针 和 跨度
  • backward:跳跃表本身基于链表,通过该属性获取前一个 zskiplistNode 结构节点
  • score:跳跃表必须包含可以判断大小的字段,通过该字段进行排序
  • obj:记录元素属性值

level

每个跳跃表节点 level 数组的长度是随机的,redis 跳跃表默认最大长度为32。一般情况下,数组的长度越大,跳跃表的查询效率越高,关于其中原理可以参考概述中引用的博客内容。

level 数组元素的 forward 属性是指向 zSkipListNode 结构的指针,通过它指向后面的跳跃表节点,需要注意的一点是:forwared 属性指向包含该层级的第一个后续节点

下面我举个简单的例子,假设现在存在5个跳跃表节点,它们随机出的 level 数组长度分别为 5、3、1、4,5。下面我通过简单图片描述该关系:

指针关系
上图是一个抽象视图,我主要想表达 level 数组指针指向包含当前层级的下一个节点。

假设此时我们需要查询元素C:从元素A开始,根据最高层数组指针,直接判断元素E,发现元素E不是所求后,回到元素A。根据次高层指针,判断元素D不为所求后,回到元素A…依次一层一层向下遍历。这里我省略了根据 score 属性判断的过程,主要想说明:跳跃表遍历总是从 最高层数组 指针所形成的链表开始,依次向下遍历

如果说 forwared 属性是指向包含当前层的下一个节点,那么 span 属性就是用来记录当前节点和被指向节点的距离。

就拿上图来说,我简单列举出几个元素数组元素的 span 值:

假设下标从1开始,和上图方块相对应。
A元素:
level[5].span = 4
level[4].span = 3
level[3].span = 1

B元素:
level[3].span = 2
level[2].span = 2
level[1].span = 1
...

span 属性值越大,说明两个节点越远。它的值可以通过下层数组元素的 span 属性加合得到,而 level[1].span 除尾节点外,总是等于1。

遍历链表时,只需要 forwared 属性即可,span 属性主要用来计算节点间距离。


backward

backward 属性表示后退指针,通过该属性获取上一个节点。需要注意的一点是:除头节点外,其他节点总是指向 level 数组中 span 属性为1的节点,也就是最底层链表的前驱节点。具体我们看示例:
backward
通过 backward 属性,redis 将链表改造为双向链表,方便从尾部向前遍历。


score 和 obj

在前面关于跳跃表的博客中,我们提到跳跃表必须包含可以用来排序的属性。否则,跳跃就失去了意义,使用跳跃表不会带来任何效率提升。

  • redis 使用 double 类型的 score 属性作为节点元素排序的基础,score 属性较小的元素排在链表前面,score 属性较大的元素排在后面。

  • redis 使用 obj 保存节点元素属性,除了用来排序的属性外,其他属性都可以在 obj 中进行记录。

下面我通过简单抽象示图描述其关系:
排序
总结一下:obj 主要记录属性,score 主要用来排序。查找元素时根据 level 数组指针遍历,遍历过程中通过 score 属性判断向前遍历,还是退回到原节点,通过数组下一层组成的链表遍历。


zSkipList

zSkipList 的结构如下图所示:

typedef struct zskiplist {
    // 表头节点和表尾节点
    structz skiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;
  • header:指向跳跃表头节点
  • tail:执行跳跃表尾节点
  • length:记录跳跃表的长度
  • level:记录跳跃表中最高的节点层数

通过该结构可以快速获取跳跃表常用属性,让跳跃表长度,头尾节点以及最高层级的获取不再成为性能瓶颈。


redis 跳跃表示例

有了上面铺垫,我们来看 redis 跳跃表具体实例图:
redis 跳跃表
这里我列举出 redis 跳跃表和常见跳跃表的不同:

  • redis 跳跃表包含前缀指针,底层链表是双向的
  • redis 跳跃表的头指针不保存数据,仅仅用来开头
  • redis 跳跃表包含额外结构记录跳跃表长度、最高层级以及头尾节点指针
  • redis 跳跃表可以包含相同 score 的元素,但 obj 属性必须唯一
  • redis 跳跃表在 score 属性相同时,根据 obj 大小判断。根据不同的场景,实现不同的 obj 大小比较方法

个人理解 redis 跳跃表设置为双向链表主要为了方便查询范围数据:如获取 score 小于某个值的节点时,找到小于它的最大节点,根据 score 属性依次向前遍历即可。除此之外,可以方便获取值 从大到小 排序的链表。

相比传统跳跃表,redis 跳跃表采用以空间换时间的方式提升部分操作的效率。需要特别注意的一点是:redis 跳跃表的头指针指向 level 长度为32、其它属性为空的跳跃表节点


跳跃表常用API

下面我通过图的形式,列举出 redis 跳跃表常见api:

跳跃表常见api


参考:
《redis设计与实现》黄健宏著
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值