redis源码解析–跳跃表机制
前言
跳表是redis里面一种常用的数据结构,它用于存储有序数据。针对于传统的顺序表、链表和树形结构,它能在数据查找、区间查找和动态变化之间达到O(logN)的复杂度。
顺序表:查找O(logN);插入删除O(N),因为会产生数据的移动。
链表:查找O(N),只能链表头部或尾部开始查找;插入删除O(1)。
AVL树或红黑树:查找、插入和删除都是O(logN);但是对于区间查找很慢,比如在10000个数据里面查找第5000大到第5100大的数据就只能使用中序遍历的方式了,这样的复杂度是O(N)。在游戏中经常会需要查看自己的排名,这些复杂度就比较高了,但好的是,游戏里面的排名不会太多,通常来看就几百个排名。
跳表:兼容了顺序表和链表的特性,对于常规查找、区间查找、插入和删除复杂度都不高于O(logN)。
一、跳跃表原理解析
跳表是一种存储有序数据的数据结构,它有如下特点:
1、是一种链式存储结构
2、表中的数据是有序的
3、查找、插入和删除复杂度均不高于O(logN)。
如下图所示,这是一个常规链式的存储结构,如果要查找73的这个数据那么就要从头开始查找,那么就需要查找3->13->23->33->43->53->63->73,一共查找8次才能找到。
如果我们考虑用二分法的办法将该列表一分为二查找数量就少了一半了,尝试一下。如下图所示,我们引入了第二层指针,如果还是查找73,那么查询过程就是43->53->63->73,注意这个过程有一个tail的对比。查询是从最高层开始查找,第一个节点查找到43,发现43小于73,然后继续往后查找,发现了tail或者大于73的数,于是降层级,查第一层,从43节点往后查,一共查了4次。
继续优化,如下图所示,查找73,查询过程为43->63->73,一共查找了3次。
可以看到每一次优化的思路如同二分查找长度减半。所以查找的次数跟查找的层数成正比,为O(logN)。下面针对于redis的源码进行具体分析。
二、源码解析
1.数据结构
跳表的数据结构如下:
typedef struct zskiplist {
struct zskiplistNode *header, *tail; //跳表的头结点和尾结点,头结点有若干层,每一层指向该层的第一个结点
PORT_ULONG length; //跳表中元素的个数
int level; //跳表中的层级,可以理解为跳表中元素的最高层级,或者头部结点的层级
} zskiplist
跳表中单个数据结点的结构如下:
typedef struct zskiplistNode { //todo 这里做得不够,需要字节对齐
robj *obj; //结点中存贮的元素
double score; //用于结点排序的值
struct zskiplistNode *backward; //指向第一层前一个结点的指针
struct zskiplistLevel {
struct zskiplistNode *forward; //每一层指向的下一个元素
unsigned int span; //在本层中,距离下一个结点的步长
} level[]; //每个结点存在多层,每一层都存在一个指针
} zskiplistNode;
将上面的链表套用到下面的数据结构示意图如下:
以43这个结点为例,backward指针指向前一个33的结点。第一层数据是距离改层的步长,44和33的步长是1,所以span=1,forward指向该层下一个结点,所以forward执向53的结点;同理第二层上一个结点是13,步长为3,所以span=3;第三层上一个结点是head,步长为5,所以span=5。
2.创建结点
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
//创建结点,这里大小为obj指针 + score + backward指针 + 数组的长度。todo 这样看其实内存利用率并不高。
zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
zn->score = score;
zn->obj = obj;
return zn;
}
3.创建跳表
#define ZSKIPLIST_MAXLEVEL 32 //元素的最大层数为32层
#define ZSKIPLIST_P 0.25 //元素每增加一层的概率是0.25
//所以这个跳表中元素的最大存储量是 4^32
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl)); //开辟一个跳表的内存
zsl->level = 1; //当前没有结点,所以层数只有一层
zsl->length = 0; //没有结点,所以数量只有一个
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); //创建一个头结点
//初始化等级数组,
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
//初始化尾部指针和头部的backward
zsl->header->backward = NULL;
zsl->tail = NULL;
return zsl;
}
现在整个跳表的图示如下:
4.插入数据结点
插入过程比较复杂,在插入代码后面都会对整个插入过程做一个图解。先看带浏览一下主要流程
//向zsl的跳表中插入结点,其值为obj,对比值为score
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
//update为插入结点时,每一层中指向新加结点的结点,最多32个, x为中间变量
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
//rank为每一层中结点需要改变的span值
unsigned int rank[ZSKIPLIST_MAXLEVEL];
//i为中间变量,level为新结点的层数
int i, level;
//这里断言score是一个可以比较的数
redisAssert(!isnan(score));
x = zsl->header;
//这个过程是为了找到新增结点每一层的上一层
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
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 &&
compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
}
//给新增结点随机化一个层次
level = zslRandomLevel();
//如果层数超过了最高层,初始化头结点新增的层数
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = (unsigned int) zsl->length;
}
zsl->level = level;
}
//创建结点并将结点加入到跳表中
x = zslCreateNode(level,score,obj);
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
//如果新增结点的层数小于跳表的层数,那么前置结点大于新增结点层数的结点span值都会加1
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
//更新backforward值
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
//跳表中结点个数加1
zsl->length++;
return x;
}
插入步骤详解,我认为整个步骤是最详细的
对于代码,一行一行的解释
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
redisAssert(!isnan(score));
x = zsl->header;
这段代码执行之后,图解如下:
继续执行代码,这段代码的逻辑是找出新结点每一层的上游。
//当前zsl->level=1
for (i = zsl->level-1; i >= 0; i--) {
//i=0 所以本行执行完成后rank[0]=0
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
//上图可以看到x指向头结点,头结点第一个位置的forward为null,所以该循环不执行
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
//将x赋值为update的第零个位置
update[i] = x;
}
执行完上述代码后,逻辑图如下,这个过程只改变了update第一个位置的指针和rank第一个位置的值。
继续执行代码
level = zslRandomLevel();
这行代码的意义是给新插入的结点一个层数,这个层数是随机的,我们来看具体实现。
#define ZSKIPLIST_P 0.25 //元素每增加一层的概率是0.25
//这个地方的复杂度很低,仅仅为O(logZSKIPLIST_MAXLEVEL)
int zslRandomLevel(void) {
//初始化层级为1层
int level = 1;
//每增加一层的概率是0.25
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
//最大层数是32层
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
通过这个随机成熟,不难发现,从第一层到最高层,每增加一层,元素的数量大约剩下25%,因此在每一层查找后,查找区间就会减小一大半。这个地方为什么会引入这个随机数,而不是在数据跳表中每四个元素增加一层,原因还是如果这样做的话,就相当于一个平衡的四叉树了,过程会十分复杂,一个数据的插入很可能会引起整个树形结构的变动。而这样做最多只会影响该结点层数个的结点发生变化。当然这里也可以使用每添加四个数据然后加一层,16个数据加2层,更高层以此类推。
继续看代码
//如果新结点的层数比跳表的层数还要高,那么就要初始化头结点中新增加的层数,并将这些层数指向新结点,因为新层数中只有这一个结点
//2 > 1
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
// i = 1 rank[1]=0
rank[i] = 0;
//头结点在该层指向新增结点
update[i] = zsl->header;
//更新头结点新增层数的span值
update[i]->level[i].span = (unsigned int) zsl->length;
}
//更新跳表的层数
zsl->level = level;
}
执行上述代码后,跳表的结构如下:
这个过程只是修改了新增层数后跳表的属性。继续看代码
//创建新结点,并给新结点赋予新的层数 2
x = zslCreateNode(level,score,obj);
//需要将新结点加入到跳表的链中,并且每一层都要指向和被指向
for (i = 0; i < level; i++) {
//这里每层都是一个单链表的插入行文
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
//新增结点的每一层都要更新span值
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
//指向新结点每一层的结点都需要更新span值
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
执行上述代码后,逻辑如下图所示,我们发现新建的结点已经链进了跳表中了
继续看代码:
//这里是为了更新前置前置结点的span值
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
//更新新结点的backward,指向第一层前面的结点
x->backward = (update[0] == zsl->header) ? NULL : update[0];
//更新前置结点的backward
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
//跳表中的元素个数加1
zsl->length++;
return x;
执行完之后,第一个结点也插入进来了,示意图如下:
尾指针指向了最后一个元素,且跳表中元素的个数加1。
依次插入33、43、83、3、63、23、13、73、53,接着我们会加入78这个结点,然后一步一步讲解插入的过程。跳跃表继续插入元素,在插入了九个元素之后,整个跳跃表的结构如下图所示,跳跃表的层数为3层。在跳跃表中只有63结点的层数为3层,33、43、73和83层数为两层,其他均为一层。
继续插入结点78,首先查找到78号结点应该插入的位置,然后在将其插入。查找过程如下:
1、从跳表的头结点中最高一层向右查找,找到63号结点
2、因为63小于78,所以继续向后查找,找到了空结点,于是层数下降一层,从第二层向后查找
3、在第二层中找到了结点73,小于78,继续向后查找
4、查找到了83,由于83大于78,所以在73号元素层数向下降一层,从第一层开始查找
5、继续向后,找到了元素又是83,因为已经是第一层了,所以73号元素是小于83号元素的最大结点了,因此83号结点需要插在73号结点的后面。
下面来看具体代码,还是一行一行地阅读,对于之前说过的逻辑进行了选择性的略去。
//第一次循环最高层从头结点开始
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 &&
compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
}