跳表skiplist
结构
Redis 中的跳跃表由 server.h/zskiplistNode
和 server.h/zskiplist
两个结构定义,前者为跳跃表节点,后者则保存了跳跃节点的相关信息,同之前的 集合 list
结构类似,其实只有 zskiplistNode
就可以实现了,但是引入后者是为了更加方便的操作:
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;// value
double score;// 分值
struct zskiplistNode *backward; // 后退指针 pre
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针 next
unsigned long span; // 跨度
} level[];// 层 //柔性数组
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 跳跃表头指针
unsigned long length; // 表中节点的数量
int level; // 表中层数最大的节点的层数
} zskiplist;
初始化
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
// 申请内存空间
zsl = zmalloc(sizeof(*zsl));
// 初始化层数为 1
zsl->level = 1;
// 初始化长度为 0
zsl->length = 0;
// 创建一个层数为 32,分数为 0,没有 value 值的跳跃表头节点
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
// 跳跃表头节点初始化
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
// 将跳跃表头节点的所有前进指针 forward 设置为 NULL
zsl->header->level[j].forward = NULL;
// 将跳跃表头节点的所有跨度 span 设置为 0
zsl->header->level[j].span = 0;
}
// 跳跃表头节点的后退指针 backward 置为 NULL
zsl->header->backward = NULL;
// 表头指向跳跃表尾节点的指针置为 NULL
zsl->tail = NULL;
return zsl;
}
插入
t_zset.c/zslInsert
第一部分:声明需要存储的变量
// 存储搜索路径
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
// 存储经过的节点跨度
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
第二部分:搜索当前节点插入位置
插入一个节点
update:要变化的是,每一层中 最后一个比插入节点分数小的节点,变化的可能是span,也可能是指向(如果插入节点的层级够了的话,需要重建该层链表的指向),图中红色即对应的update节点。因为插入8节点的level只有1,update的0号是要重建链表指向的,123号则只修改span的值。
rank:span 是为了排名而增加的字段,因为插入会让在插入节点后所有节点排名+1,保存每个节点的排名是不可能的 (不然第一名易主,后面所有人的排名都要跟着变),可以的是保存节点和下一个节点排名的间隔(span),游戏中的实时排行榜记录的一定是排名的间隔,把排名的变化缩小到前后节点的局部变化。
serverAssert(!isnan(score));
x = zsl->header;
// 逐步降级寻找目标节点,得到搜索路径
for (i = zsl->level-1; i>=0; i--)
{
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
while(x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score && sdscmp(x->level[i].ele, ele) < 0) ))
{
rank[i] += x->level[i].scan;
x = x->level[i].forward;
}
// 记录搜索路径
update[i]=x;//出循环时,x的下一个节点分数比要插入的高,所以需要修改的是x
}
第三部分:生成插入节点
/* we assume the element is not already inside, since we allow duplicated
* scores, reinserting the same element should never happen since the
* caller of zslInsert() should test in the hash table if the element is
* already inside or not. */
level = zslRandomLevel();
// 如果随机生成的 level 超过了当前最大 level 需要更新跳跃表的信息
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;
}
zsl->level = level;
}
// 创建新节点
x = zslCreateNode(level,score,ele);
第四部分:重排前向指针
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward;//4-1
update[i]->level[i].forward = x;//4-2
/* update span covered by update[i] as x is inserted here */
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);//4-3
update[i]->level[i].span = (rank[0] - rank[i]) + 1;//4-4
}
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;//4-5
}
第五部分:重排后向指针并返回
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++;
return x;
删除
删除过程由源码中的 t_zset.c/zslDeleteNode
定义,和插入过程类似,都需要先把这个 “搜索路径” 找出来,然后对于每个层的相关节点重排一下前向后向指针,同时还要注意更新一下最高层数 maxLevel
,直接放源码 (如果理解了插入这里还是很容易理解的):
删除一个节点
update:要变化的是,每一层中 最后一个比插入节点分数小的节点,变化的可能是span,也可能是指向(如果删除节点的层级够了的话,需要重建该层链表的指向),图中红色即对应的update节点。因为删除的8节点的level只有1,update的0号是要重建链表指向的,123号则只修改span的值。
如果插入一个节点后,立刻删除该节点,两次的搜索路径其实是相同的。
/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
int i;
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) {
update[i]->level[i].span += x->level[i].span - 1;
update[i]->level[i].forward = x->level[i].forward;
} else {
update[i]->level[i].span -= 1;
}
}
if (x->level[0].forward) {
x->level[0].forward->backward = x->backward;
} else {
zsl->tail = x->backward;
}
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--;
zsl->length--;
}
/* Delete an element with matching score/element from the skiplist.
* The function returns 1 if the node was found and deleted, otherwise
* 0 is returned.
*
* If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise
* it is not freed (but just unlinked) and *node is set to the node pointer,
* so that it is possible for the caller to reuse the node (including the
* referenced SDS string at node->ele). */
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[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) {
zslDeleteNode(zsl, x, update);
if (!node)
zslFreeNode(x);
else
*node = x;
return 1;
}
return 0; /* not found */
}
更新
当我们调用 ZADD
方法时,如果对应的 value 不存在,那就是插入过程,如果这个 value 已经存在,只是调整一下 score 的值,那就需要走一个更新流程。
假设这个新的 score 值并不会带来排序上的变化,那么就不需要调整位置,直接修改元素的 score 值就可以了,但是如果排序位置改变了,那就需要调整位置,该如何调整呢?
从源码 t_zset.c/zsetAdd
函数 1350
行左右可以看到,Redis 采用了一个非常简单的策略:
/* Remove and re-insert when score changed. */
if (score != curscore) {
zobj->ptr = zzlDelete(zobj->ptr,eptr);
zobj->ptr = zzlInsert(zobj->ptr,ele,score);
*flags |= ZADD_UPDATED;
}
把这个元素删除再插入这个,需要经过两次路径搜索,从这一点上来看,Redis 的 ZADD
代码似乎还有进一步优化的空间。
排名
跳跃表本身是有序的,Redis 在 skiplist 的 forward 指针上进行了优化,给每一个 forward 指针都增加了 span
属性,用来 表示从前一个节点沿着当前层的 forward 指针跳到当前这个节点中间会跳过多少个节点。在上面的源码中我们也可以看到 Redis 在插入、删除操作时都会小心翼翼地更新 span
值的大小。
所以,沿着 “搜索路径”,把所有经过节点(比查找节点分数小的节点)的跨度 span
值进行累加,直到找到 就可以算出当前元素的最终 rank 值了。
从表中也可以看到,从一个点到另一个点,不走回头路的情况下,span和一定是相同的。
/* Find the rank for an element by both score and key.
* Returns 0 when the element cannot be found, rank otherwise.
* Note that the rank is 1-based due to the span of zsl->header to the
* first element. */
unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
zskiplistNode *x;
unsigned long rank = 0;
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))) {
// span 累加
rank += x->level[i].span;
x = x->level[i].forward;
}
/* x might be equal to zsl->header, so test if obj is non-NULL */
if (x->ele && sdscmp(x->ele,ele) == 0) {
return rank;
}
}
return 0;
}