复习《redis设计与实现》,跳跃表是在看《redsi设计与实现》的时候第一次接触到的概念,因此认真研究了一下跳跃表的性质以及redis源码中针对跳跃表的实现。下面对redis源码中关于跳跃表的实现配合源码做详解的解释(注释都包含在源码中)。至于跳跃表的性质及查找的复杂度(平均为O(log(N))、最差为O(N)),自行google,相关文章非常之多。本篇文章只记录当时自己花费时间比较多去理解的部分。
1、跳跃表的数据结构
在redis中实现的跳跃表使用了跳跃表zskiplist和跳跃表节点在zskiplistNode两种struct。下面分别进行解释。(在redis 4.0.1版本中zskiplistNode和zskiplist定义在server.h中,并非redis3.0中的redis.h中,如果读者阅读源码,查找)。
·zskiplistNode结构体
typedef struct zskiplistNode {
/* redis3.0版本中使用robj类型表示,但是在redis4.0.1中直接使用sds类型表示 */
sds ele;
double score;
struct zskiplistNode *backward;
/** 这里该成员是一种柔性数组,只是起到了占位符的作用,在sizeof(struct zskiplistNode)的时候根本就不占空间,这和sdshdr结构的定义是类似的(sds. h文件); 如果想要分配一个struct zskiplistNode大小的空间,那么应该的分配的大小为sizeof(struct zskiplistNode) + sizeof(struct zskiplistLevel) * count)。其中count为柔性数组中的元素的数量
**/
struct zskiplistLevel {
/* 对应level的下一个节点 */
struct zskiplistNode *forward;
/* 从当前节点到下一个节点的跨度 */
unsigned int span;
} level[];
} zskiplistNode;
·zskiplist结构体
typedef struct zskiplist {
/* 跳跃表的头结点和尾节点,尾节点的存在主要是为了能够快速定位出当前跳跃表的最后一个节点,实现反向遍历 */
struct zskiplistNode *header, *tail;
/* 当前跳跃表的长度,保留这个字段的主要目的是可以再O(1)时间内获取跳跃表的长度 */
unsigned long length;
/* 跳跃表的节点中level最大的节点的level保存在该成员变量中。但是不包括头结点,头结点的level永远都是最大的值---ZSKIPLIST_MAXLEVEL = 32。level的值随着跳跃表中节点的插入和删除随时动态调整 */
int level;
} zskiplist;
2、跳跃表中增、删、查的实现
·删除整个跳跃表,并释放分配的空间
void zslFree(zskiplist *zsl) {
/** 根据跳跃表的性质,跳跃表的最低一层也就是level[0]的一层包含了跳跃表中的所有节点。因此,只需要依次释放掉level[0]中forward指针所连接的节
点即可 **/
zskiplistNode *node = zsl->header->level[0].forward, *next;
zfree(zsl->header);
while(node) {
next = node->level[0].forward;
zslFreeNode(node);
node = next;
}
/** 释放掉zskiplist结构体 **/
zfree(zsl);
}
·向跳跃表中插入节点
/** 将新的节点插入到zskiplist中。在调用该函数之前,由调用者确保该成员在zskiplist中不存在 **/
/** 该程序不容易理解的话,可以按照redis中定义的zskiplist结构,将各种情况走一遍 **/
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
/* update数组保存了要插入的新节点在插入之后各个level的紧邻的前边的节点 */
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
/* rank数组保存在新节点插入之后所在位置的各个level的前面紧邻节点在整个跳跃表中的排位,用于更新各个level的前面紧邻节点的跨度和新节点到后面
紧邻节点的跨度 */
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
/** 如果!isnan(score)为0,那么将记录相关的错误信息,exit(1)退出程序 **/
serverAssert(!isnan(score));
x = zsl->header;
/** 下面的for循环是为了确定新的zskiplistNode节点应该插入的位置的前面的节点 x **/
/** 按照在跳跃表中查找给定节点的顺序,应该是按照forward和down的顺序进行查找。首先是从最高层开始向后进行查找,如果当前节点的forward指针指向
的节点的score小于给定的score或者说forward指向的节点的score等于给定的score但是forward指向的节点的ele的字典序小于给定的ele,应该继续向后进行查> 找,否则应该向下(down)进行查找,对应于redis中的实现,zskiplist实现中,应该将层级减 1。在跳跃表中,在较高的层出现的zskiplistNode在较低的层肯定
会出现;反之却不一定 **/
/** 注意这里的 i 是从zsl->level-1开始的,而不是从最大的ZSKIPLIST_MAXLEVEL开始的 **/
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
/** 初始化每一层的rank。如果是最高的一层,则初始化为0;如果不是最大的level,则证明是由上一层级down下来的,应该初始化为上一层的rank **/
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
/** x代表在第 i 层要插入的节点的backward指针指向的节点;每层或者都不一样或者也有可能是相同的 **/
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
/** update保存了新插入的节点位置在第 i 层的前向节点 **/
update[i] = x;
}
level = zslRandomLevel();
/** 如果随机产生的新的层级大于当前的zsl中保存的level(当前zsl的最大层级),那么新的层级的backward节点全部指向zsl->header;此时当然在update> 中保存的也是update[i](i > zsl)应该是zsl->header; **/
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
/** 只是暂时的一个初始化,后面会进行相应的修改 **/
update[i]->level[i].span = zsl->length;
}
/** 新插入的zskiplistNode的层大于现有的zsl->level,更改现有的zsk->level **/
zsl->level = level;
}
x = zslCreateNode(level,score,ele);
for (i = 0; i < level; i++) {
/* 更新新插入的节点在每层的forward节点指向的节点 */
x->level[i].forward = update[i]->level[i].forward;
/* 更新新插入的节点所在位置前面紧邻节点的forward指向新插入的节点 */
update[i]->level[i].forward = x;
/* 改变由于插入新的zskiplistNode导致的跨度的变化 */
/** rank[0] 代表了要插入的 zskiplistNode 在实际的链表中的排位,rank[i]代表了在相应的层级的排位 **/
/** 跨度如果不好明白的话,可以画图理解 **/
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* 如果新插入的节点的level小于原来的zsl->level,那么那些大于新插入节点level的level并没有指针指向新插入的节点,此时只需简单的将其跨度增加 1 即可 */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
/** header 节点的forward节点的backward节点不再指向header,而是指向NULL **/
x->backward = (update[0] == zsl->header) ? NULL : update[0];
/** 考虑到将新的zskiplistNode节点添加到最后一个节点的情况 **/
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++;
return x;
}
个人认为,跳跃表的整个难点就在于插入操作中。明白了插入操作的代码做执行的每一步操作,那么再去理解查找和删除的代码操作就很容易了。其中插入操作的关键点在于确定要新插入的节点所在的位置,所说的这种位置包括来那个方面,第一是新插入的节点在各个level的前面紧邻接点;第二,新插入的节点的各个level的前面紧邻节点在整个跳跃表中的rank。就是上面代码中加粗的部分,如果不理解,根据《redis设计与实现》中的图画图进行理解。这里不再进行画图的操作。
·在跳跃表中查找节点
zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) {
zskiplistNode *x;
int i;
/* If everything is out of range, return early. */
if (!zslIsInRange(zsl,range)) return NULL;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
/* Go forward while *OUT* of range. */
/** 找到第一个大于(或者等于,具体取决于range->minex是否为1)range->min的节点的前向节点并记录下来 **/
while (x->level[i].forward &&
!zslValueGteMin(x->level[i].forward->score,range))
x = x->level[i].forward;
}
/* This is an inner range, so the next node cannot be NULL. */
/* 根据条件而言,x 不可能为NULL */
x = x->level[0].forward;
serverAssert(x != NULL);
/* Check if score <= max. */
if (!zslValueLteMax(x->score,range)) return NULL;
return x;
}
其他在跳跃表中查找给定范围条件的函数,zslLastInRange()、zslFirstInLexRange()和zslLastInlexRange()都和上面的zslFirstInRange()原理相同,在此不再进行赘述。
·跳跃表中删除节点
在跳跃表中删除节点为了能够维持跳跃表的完整性(同步跟新forward指针、backward指针和span成员)在进行实际的删除操作之前应该按照跳跃表中查找元素的算法(forward->down)确定要删除的节点位置在每一level的前面节点。下面以zslDelete()为例进行介绍。
/* node 指向要删除的节点的指针的指针;如果不需要返回被删除的节点,那么直接将该参数置为NULL即可,如果希望返回被删除的节点,那么该参数不能为空
*/
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i;
/* 确定要删除的节点在每一层的前面的紧邻节点 */
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
x = x->level[i].forward;
}
/** update保存在该节点在每一层级的前向节点 **/
update[i] = x;
}
/* We may have multiple elements with the same score, what we need
* is to find the element with both the right score and object. */
x = x->level[0].forward;
if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
/* unlink from current zskiplist */
zslDeleteNode(zsl, x, update);
/* free the node dirctely */
if (!node)
zslFreeNode(x);
else
*node = x;
return 1;
}
return 0; /* not found */
}
/* node 指向要删除的节点的指针的指针;如果不需要返回被删除的节点,那么直接将该参数置为NULL即可,如果希望返回被删除的节点,那么该参数不能为空
*/
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i;
/* 确定要删除的节点在每一层的前面的紧邻节点 */
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
x = x->level[i].forward;
}
/** update保存在该节点在每一层级的前向节点 **/
update[i] = x;
}
/* We may have multiple elements with the same score, what we need
* is to find the element with both the right score and object. */
x = x->level[0].forward;
if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
/* unlink from current zskiplist */
zslDeleteNode(zsl, x, update);
/* free the node dirctely */
if (!node)
zslFreeNode(x);
else
*node = x;
return 1;
}
return 0; /* not found */
}
在上面使用了zslDeleteNode(),下面我们分析其源码实现。
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
int i;
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) {
/** 将x的跨度转移到x在第i层的节点update[i]上,同时减少的跨度 1 为 x 节点本身 **/
update[i]->level[i].span += x->level[i].span - 1;
/* update保存了x在各个层前面的紧邻节点,因此需要更新update[i]->level[i].forward为x->level[i].forward,将x从跳跃表中接触链接 */
update[i]->level[i].forward = x->level[i].forward;
} else {
/* 如果x在第 i 层前面并没有别的节点指向x,那么直接将跨度 -1 即可;无需在调整指针的指向 */
update[i]->level[i].span -= 1;
}
}
if (x->level[0].forward) {
x->level[0].forward->backward = x->backward;
} else {
zsl->tail = x->backward;
}
/** 最高层的节点的数量永远是最少的,因此删除的时候可能会将level减1 **/
/** 头结点的level[i].forward如果变为NULL值,证明该level已经不存在任何的节点了,将zsl->level - 1; **/
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--;
zsl->length--;
}
上面我们说了在跳跃表中删除单个节点的过程,但是像zremrangebyscore,zremrangebylex命令都是要删除在给定范围内的多个元素的,因此这里以zremrangebyscore的实现函数zslDeleteRangeByScore为例进行说明,其中重点说明的是如何连续删除在一定范围内的多个节点。至于找到第一个在给定范围内的节点的算法,前面在介绍zslFirstInRange的时候介绍过了,原理都是一样的。
unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, dict *dict) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned long removed = 0;
int i;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward && (range->minex ?
x->level[i].forward->score <= range->min :
x->level[i].forward->score < range->min))
x = x->level[i].forward;
update[i] = x;
}
/* Current node is the last with score < or <= min. */
x = x->level[0].forward;
/* Delete nodes while in range. */
while (x &&
(range->maxex ? x->score < range->max : x->score <= range->max))
{
zskiplistNode *next = x->level[0].forward;
/** 从前向后删除的过程中,update中的值并不会发生变化;比如删除第一个满足要求的节点X只有,x->forward->backward就变成了原来x->backward * */
zslDeleteNode(zsl,x,update);
dictDelete(dict,x->ele);
zslFreeNode(x); /* Here is where x->ele is actually released. */
removed++;
x = next;
}
return removed;
}
在连续删除的过程中,每次调用zslDeleteNOde()之后都会保持原跳跃表的完整性,将原来x->level[i]->forward位置的节点提到了原来的x的位置。所以,如果下一个节点(假设为y)还是满足要求,那么y在每一level的前面紧相邻的节点仍然是update[i],每次调用zslDeleteNode的时候update的值并不需要变化。
以上,是个人关于redis源码中zskiplist的实现的理解,欢迎吐槽。