Redis设计与实现-跳跃表

Redis跳跃表的实现。

用到的两个结构体redis.h/zskiplistNoderedis.h/zskiplist两个结构定义,其中zskiplistNode结构表示跳跃表节点,而zskiplist结构则是用于保存跳跃表节点的相关信息,具体的以下有讲解。

一个跳跃表的例子

跳跃表示例

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

  • header 指向跳跃表的表头节点。

  • tail 指向跳跃表的表尾节点。

  • level 记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。

  • length 记录跳跃表的长度,也就是跳跃表目前包含节点的数量(表头节点不包含在内)。

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

  • 层:节点中使用 L1,L2,L3 等字样标记节点的各个层。每个层都带有两个属性:前进指针和跨度。
    • 前进指针:用于访问位于表尾方向的其他节点。(上图中带有数字的箭头表示前进指针)
    • 跨度:记录了前进指针所指向节点和当前节点的距离。(线条上的数字表示跨度)
  • 后退指针:节点中使用 BW 标记节点的后退指针,指向当前节点的前一个节点,后退指针在程序从表尾向表头遍历时使用。
  • 分支:各个节点中的 1.0,,2.0,3.0 是节点所保存的分值,在跳跃表中,节点按照各自所保存的分值从小到大排列。
  • 成员对象:各个节点中的 o1,o2,o3是节点所保存的成员对象。

PS:表头节点和其他节点是一样的,只是表头节点的其他属性不会用到,故图中忽略了。

跳跃表节点

redis.h/zskiplistNode 结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct zskiplistNode{
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
//层
struct zskiplistlevel{
//前进指针
struct zskiplistNode * forward;
//跨度
unsigned int span;
}level[];
}zskiplistNode;

跳跃表节点的 level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,理论上,层的数量越多,访问其他的节点速度就越快。每次创建一个新跳跃表节点的时候,程序都根据幂次定律(越大的数出现的概率越小)随机生成一个介于1到32之间的值作为 level 数组的大小,这个大小就是层的高度。如下图所示:

skiplist_2

前进指针

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

遍历跳跃表所有节点的路径:

  1. 迭代程序首先访问跳跃表的第一个节点(表头),然后从第四层的前进指针移动到第二个节点。
  2. 在第二个节点时,程序沿着第二层的前进指针移动到表中的第三个节点。
  3. 在第三个节点时,程序同样沿着第二层的前进指针移动到表中的第四个节点。
  4. 当程序再次沿着第四个节点的前进指针移动时,碰到一个 NULL ,此时走到了跳跃表的表尾,结束此次遍历。

遍历跳跃表流程如图:

skiplist_3

跨度

层的跨度用于记录两个节点之间的距离:

  • 两个几点之间的跨度越大,他们相距的就越远。
  • 指向 NULL 的所有前进指针的跨度都为0,因为它们没有连向任何节点。

看到这个属性,很容易认为这个属性跟遍历有关,实际上遍历只需要前进指针即可,这个跨度是用来计算节点所在跳跃表中的排位的,查找某个节点的时候累加过程中的所有跨度就能算出该节点在表中的排位。

例如:如图,现在查找 成员对象是 o3 的节点,沿途经过一个层,即跨度之和 = 1 + 2,所以目标节点在跳跃表中的排位是3。

skiplist_4

后退指针

节点的后退之后(backward 属性)用于从表尾向表头方向访问节点, 每个节点只有一个后退指针,每次只能后退至前一个节点。表尾向表头遍历跳跃表所有节点:通过跳跃表 tail 指针访问表尾节点,然后通过后退指针访问每一个节点,直到遇到 NULL 表示遍历结束。

分值和成员

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

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

在同一个跳跃表中,各个节点保存的成员对象是唯一的,但是多个节点可以保存相同的分值,分值相同的节点则按照成员对象在字典序中的大小来排序,成员对象较小的节点会排在前面(靠近表头),反之排在后面。

跳跃表

虽然通过多个跳跃节点就可以构成一个跳跃表,但是通过使用一个 zskiplist 结构来持有这些节点,程序对跳跃表的处理更加方便。

zskiplist 结构定义如下:

1
2
3
4
5
6
7
8
typedef struct zskiplist {
//表头节点和表尾节点
struct zskiplistNode *head, *tail;
//表中节点数量
unsigned long length;
//表中层数最大的节点层数
int level;
}zskiplist;

headtail 指针分别指向跳跃表的表头和表尾。

length 属性记录了表中节点的数量。

level 属性记录了表中层数最高的那个节点的层数(整个表中的最高层数),不包括头结点(表头)。

跳跃表所有操作的API

函数 作用 时间复杂度
zslCreate 创建一个新的跳跃表。 O(1)
zslFree 释放给定跳跃表,以及表中包含的所有节点。 O(N) , N 为跳跃表的长度。
zslInsert 将包含给定成员和分值的新节点添加到跳跃表中。 平均 O(log N) ,最坏 O(N) , N 为跳跃表长度。
zslDelete 删除跳跃表中包含给定成员和分值的节点。 平均 O(log N) ,最坏 O(N) , N 为跳跃表长度。
zslGetRank 返回包含给定成员和分值的节点在跳跃表中的排位。 平均 O(log N) ,最坏 O(N) , N 为跳跃表长度。
zslGetElementByRank 返回跳跃表在给定排位上的节点 平均 O(log N) ,最坏 O(N) , N 为跳跃表长度。
zslIsInRange 给定一个分值范围,,如果给定的分值范围包含在跳跃表的分值范围之内,返回1,否则返回0. O(1)
zslFirstInRange 给定一个分值范围,返回跳跃表中第一个返回符合这个范围的节点。 平均 O(log N) ,最坏 O(N) 。 N 为跳跃表长度。
zslLastInRange 给定一个分值范围,返回跳跃表中最后一个符合这个范围的节点。 平均 O(log N) ,最坏 O(N) 。 N 为跳跃表长度。
zslDeleteRangeByScore 给定一个分值范围,删除跳跃表中所有在这个范围之内的节点。 O(N) , N 为被删除节点数量。
zslDeleteRangeByRank 给定一个排位范围,删除跳跃表中所有在这个范围之内的节点。 O(N) , N 为被删除节点数量。

本文标题:Redis设计与实现-跳跃表

文章作者:Tokey

发布时间:2019年05月21日 - 10:05

最后更新:2021年06月29日 - 22:06

原始链接:http://TokeyRoad.github.io/2019/05/21/Redis设计与实现-跳跃表/

许可协议: 转载请保留原文链接及作者。

0%