5.1 跳跃表的实现
Redis的跳跃表由 zskiplistNode
和 zskiplist
两个结构定义。
其中,zskiplistNode
用于表示跳跃节点,而 zskiplist
结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等。
图5-1展示了一个跳跃表实例,位于图片最左边的是 zskiplist
结构,该结构包含一下属性:
- header:指向跳跃表的头节点
- tail:指向跳跃表的尾节点
- level:记录目前跳跃表内,层数最大的那个节点的层数
- length:记录跳跃表的长度
位于 zskiplist
结构右方的是四个 zskiplistNode
结构,该结构包含以下属性:
- 层:节点中使用L1、L2 、L3等字样标记节点的每个层,L1代表第一层,以此类推。每个层都含有2个属性:前进指针和跨度。前进指针用于访问表尾方向的其他节点,而跨度记录了前进指针和跨度所指节点和当前节点的距离。如图5-1所示,连线上带有数字的箭头就代表了前进指针,而那个数字就是跨度。
- 后退指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。
- 分值:各个节点的1.0、2.0、3.0是节点保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
- 成员对象:各个节点的o1、o2、o3是节点所保存的对象。
注意表头节点和其他节点的构造是一样的:表头节点也有后退指针、分值、成员对象,不过表头节点的这些属性不会被用到,所以图中省略了这些部分,只显示了表头节点的各个层。
5.1.1 跳跃表节点
跳跃表节点的实现由 zskiplistNode
结构定义:
typedef struct zskiplistNode { // 层 struct zskiplistLevel { // 前进指针 struct zskiplistNode *forward; // 跨度 unsigned int span; } level[]; // 后退指针 struct zskiplistNode *backward; // 分值 double score; // 成员对象 robj *obj; } zskiplistNode;
5.1.1.1 层
跳跃表的层数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。
每次创建一个新跳跃节点的时候,程序都会根据幂次定律(越大的数出现的概率越小)随机生成一个介于1~32之间的值作为level数组的大小,这个大小就是层的高度。
5.1.1.2 前进指针
每个层都有一个指向表尾方向的前进指针(level[i].forward属性),用于从表头向表尾方向访问节点。
- 程序首先访问第一个节点(表头),然后从第四层的前进指针移动到表中的第二个节点。
- 在第二个节点时,程序沿着第二层的前进指针移动到列表中的第三个节点。
- 在第三个节点时,程序同样沿着第二层的前进指针到达表中的第四个节点
- 当程序到达第四个节点的时候,会碰到一个NULL,程序知道已经到达了跳跃表的表尾,于是结束这次遍历。
5.1.1.3 跨度
层的跨度主要记录两个节点之间的距离:
- 两个节点之间的跨度越大,他们相距的就越远
- 指向NULL的所有前进指针的跨度都为0,因为他们没有连接任何节点。
第一眼看上去,很容易以为跨度和遍历操作有关,但实际上不是这样的。
遍历我们只需要使用前进指针就可以完成了,跨度实际上是用来计算排名的:在查找某个节点的过程中,将沿途访问过的所有层的跨度加起来,得到的结果就是目标节点在跳跃表的排位。
5.1.1.4 后退指针
节点的后退指针用于从表尾向表头方向访问节点:前进指针的话,可以一次性跳过多个节点,而我们的后退指针,一次只能回退一个节点。
5.1.1.5 分值和成员
节点的分值(score属性)是一个double类型的浮点数,跳跃表中的所有节点都按照分值从小到大进行排序。
节点的成员对象(obj属性)是一个指针,指向一个字符串对象,而字符串对象则保存着一个SDS值。
在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是各个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员较小的节点会排在前面(靠近表头的地方),而成员对象较大的节点则会排在后面(靠近表尾的方向)。
5.1.2 跳跃表
通过一个 zskiplist
结构来持有这些节点,程序可以方便的对整个跳跃表进行处理,比如:快速访问表头节点和表尾节点,或者快速的获取跳跃表的长度。
zskiplist 结构定义如下:
typedef struct zskiplist { // 表头节点和表尾节点 struct skiplistNode *header,*tail; // 表示节点的数量 unsigned long length; // 表中层数最大的节点的层数 int level; } zskiplist;
header
和 tail
指针分别指向跳跃表的表头和表尾节点,通过这两个指针,我们可以通过O(1)的时间复杂度来获取表头和表尾。
通过使用 length
属性来记录节点的数量,程序可以在O(1)的复杂度返回跳跃表的长度。
level
属性用于在O(1)的时间获取跳跃表层数最大的那个节点的层数量(不算表头的)。
5.2 跳跃表API
5.3 重点回顾
- 跳跃表是有序集合的底层实现之一。
- Redis 的跳跃表实现由
zskiplist
和zskiplistNode
两个结构构成,其中zskiplist
属性保存跳跃表的信息(比如表头节点、表尾节点、长度),而zskiplistNode
用于表示跳跃表节点。 - 每个跳跃表节点的层高都是
1~32
之间的随机数。 - 在同一个跳跃表中,多个节点可以包含相同的分值,但不能包含相同的对象。
- 跳跃表节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。