「这是我参与2022首次更文挑战的第30天,活动详情查看:2022首次更文挑战」。
Sorted-Set底层存储数据结构
3.4.2 查到插入的位置
我们假如跳表现在已经有元素1、11、23三个元素了,我们想插入一个新元素21,如图3.3所示:
我们先看一下找到插入位置的源代码:
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!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 &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
}
复制代码
查找节点(score=21,level=2)的插入位置,逻辑如下: 1)第一次for循环,i=1。x现在为跳跃表的头节点; 2)现在i的值与zsl->level-1相等,所以rank[1]*的值为*0; 3)header->level[1].forward存在,并且header->level[1].forward->score(1)*小于要插入值的*score,所以while循环可以进入,rank[1]=1,x赋值为第一个节点; 4)第一个节点的第1层的forward指向NULL,所以while循环不会再进入,经过第一次for循环,rank[1]=1,**x和update[1]**都为第一个节点(score=1)。
5)for循环进入第二次,i=0。x为跳跃表第一个节点(score=1) 6)现在i的值与zsl->level-1不相等,所以rank[0]**等于**rank[1]*的值赋值为*1; 6)x->level[0]->forward存在,并且x->level[0].foreard->score(11)小于要插入的score,所以while循环可以进入,*rank[0]=2*,x为第二个节点(score=11)。 7)**x->level[0]->forward**存在,并且x->level[0].foreard->score(23)**大于要插入的score,所以while不会再进入,经过第二次for循环,rank[0]=2,x和update[0]**都为第二个节点(score=11),如**图3.5所示:
这个时候我们就找到插入位置在元素11和元素23中间。
3.4.3 调整跳表的高度
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 = zsl->length;
}
zsl->level = level;
}
复制代码
假如我们生成的高度是3,这个时候我们需要调整一下整个跳表的高度,此时i的值是2,level的值是3,所以我们只能进入一次循环,此时rank[2] = 0,update[2]**指向头结点,**update[2]->level[2].span = 3**,**zsl->level = 3**,调整完高度,如**图3.6所示:
3.4.4 插入元素
x = zslCreateNode(level,score,ele);
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
/* update span covered by update[i] as x is inserted here */
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
复制代码
level的值为3,所以for循环可以执行三遍,插入过程如下:
for循环第一遍:
1)x的level[0]**的**forward为update[0]**的**level[0]**的**forward节点,即x->level[0].forward为score=23的节点;
2)**update[0]的level[0]**的下个节点为新插入的节点;
3)**rank[0]*的值为0,**update[0]->level[0].span=1**,所以*x->level[0].span=1、update[0]->level[0].span=1。
我们再看一下经过第一次for循环,我们的结构,如图3.7所示:
for循环第二遍: 1)x的level[1]**的**forward为update[1]**的**level[1]**的forward节点,即**x->level[1].forward为NULL; 2)**update[1]的level[1]**的下个节点为新插入的节点; 3)**rank[1]*的值为1,**update[1]->level[1].span**等于2,所以*x->level[1].span=1 4)update[1]->level[1].span=2。
我们再看一下经过第二次for循环,我们的结构,如图3.8所示:
for循环第三遍: 1)x的level[2]**的forward为**update[2]**的**level[2]**的forward节点,即**x->level[2].forward为NULL; 2)update[2]**的**level[2]*的下个节点为新插入的节点; 3)**rank[2]**的值为2,因为**update[2]->level[2].span**等于跳跃表的总长度*3,所以x->level[2].span=1**; 4)update[2]->level[2].span=3。
我们再看一下经过第三次for循环,我们的结构,如图3.9所示:
至此,我们的插入过程完成。
3.4.5 调整backward并更新跳表的长度
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++;
复制代码
x->backward的值等于,判断update[0] == zsl->header是否相等,如果想当代表是个空跳表,则赋值为NULL,如果不是的话,就把update[0]**的赋给它,也就是说指向**score = 11的backward。
第二步判断是否是尾结点,如果不是则调整对应的backward,如果是就更新跳表的尾部指向。
最后再更新跳表的length = 4,调整完的结构如图3.10所示:
好了,我们的插入元素整个源码分析到此告一段落,本小节通过分析插入元素21的过程带大家分析每一句源码的涵义,希望对大家了解插入过程有所帮助。
3.5 Sorted-Set查询一个元素源码分析
下面通过zslGetRank函数来看一下查询一个元素过程,下面是具体的源代码:
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))) {
rank += x->level[i].span;
x = x->level[i].forward;
}
/* x might be equal to zsl->header, so test if obj is non-NULL */
//如果查找到相同的元素,则直接返回,rank
if (x->ele && sdscmp(x->ele,ele) == 0) {
return rank;
}
}
//否则返回0
return 0;
}
复制代码
我们可以通过源代码来看,其实查找一个元素跟我们插入过程中查找插入位置的代码基本一致,具体查询过程此小节就不详细展开了,(可以查看插入过程查找插入位置的详细讲解)。
3.6 Sorted-Set查询多个元素源码分析
3.6.1 整体源码分析
查询多个元素我们通过zrange和zrevrange的命令的实现来进行源码分析:
void zrangeGenericCommand(client *c, int reverse) {
robj *key = c->argv[1];
robj *zobj;
int withscores = 0;
long start;
long end;
long llen;
long rangelen;
//中间省略一下无用的代码
.....
//获取跳表当前的长度,请参数小节3.9.1
llen = zsetLength(zobj);
//初始化一下开始游标和结束游标
if (start < 0) start = llen+start;
if (end < 0) end = llen+end;
if (start < 0) start = 0;
/* Invariant: start >= 0, so this test will be true when end < 0.
* The range is empty when start > end or start >= length. */
//异常判断
if (start > end || start >= llen) {
addReply(c,shared.emptymultibulk);
return;
}
//得到获取元素的个数
if (end >= llen) end = llen-1;
rangelen = (end-start)+1;
/* Return the result in form of a multi-bulk reply */
addReplyMultiBulkLen(c, withscores ? (rangelen*2) : rangelen);
//如果是压缩列表,这次我们不关心压测列表,可以先跳过这段代码
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl = zobj->ptr;
unsigned char *eptr, *sptr;
unsigned char *vstr;
unsigned int vlen;
long long vlong;
if (reverse)
eptr = ziplistIndex(zl,-2-(2*start));
else
eptr = ziplistIndex(zl,2*start);
serverAssertWithInfo(c,zobj,eptr != NULL);
sptr = ziplistNext(zl,eptr);
while (rangelen--) {
serverAssertWithInfo(c,zobj,eptr != NULL && sptr != NULL);
serverAssertWithInfo(c,zobj,ziplistGet(eptr,&vstr,&vlen,&vlong));
if (vstr == NULL)
addReplyBulkLongLong(c,vlong);
else
addReplyBulkCBuffer(c,vstr,vlen);
if (withscores)
addReplyDouble(c,zzlGetScore(sptr));
if (reverse)
zzlPrev(zl,&eptr,&sptr);
else
zzlNext(zl,&eptr,&sptr);
}
//如果是跳表
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplist *zsl = zs->zsl;
zskiplistNode *ln;
sds ele;
/* Check if starting point is trivial, before doing log(N) lookup. */
//如果逆序,则从后往前取
if (reverse) {
ln = zsl->tail;
//如果start不是从0开始,则先要找到start的那个起点的结点
if (start > 0)
//通过rank获取该结点,请参考3.9.2
ln = zslGetElementByRank(zsl,llen-start);
} else {
//从第一个结点开始
ln = zsl->header->level[0].forward;
if (start > 0)
ln = zslGetElementByRank(zsl,start+1);
}
//循环这次去除取数据的个数,直到元素取完
while(rangelen--) {
//断言
serverAssertWithInfo(c,zobj,ln != NULL);
ele = ln->ele;
//把数据写到对client的缓冲区中,批量返回请参考3.9.3
addReplyBulkCBuffer(c,ele,sdslen(ele));
//假如带了withscores参数
if (withscores)
//把结点的score(double类型)也同时写到缓冲区,请参考3.9.4
addReplyDouble(c,ln->score);
//下一个结点,如果从头那,则用forward找到下一个结点,如果是从尾部拿,则用backward找打前一个结点
ln = reverse ? ln->backward : ln->level[0].forward;
}
} else {
serverPanic("Unknown sorted set encoding");
}
}
复制代码
这个过程就非常简单了,我们通过两张图来分别描述一下从头取元素,从尾取元素的过程。
3.6.2 Zrange查询过程
命令:zrange mytest 0 -1**,从头部取元素的过程,1 -> 11 -> 21 -> 23 结束。如图3.12:
3.6.3 Zrevrange查询过程
命令:zrevrange mytest 0 -1**,从尾部取元素的过程,23 -> 21 -> 11 -> 1 结束。如图3.13: