我们再来学习如何从跳跃表中查询数据,跳跃表本质上是一个链表,但它允许我们像数组一样定位某个索引区间内的节点,并且与数组不同的是,跳跃表允许我们将头节点L0层的前驱节点(即跳跃表分值最小的节点)zsl->header.level[0].forward当成索引0的节点,尾节点zsl->tail(跳跃表分值最大的节点)当成索引zsl->length-1的节点,索引按分值从小到大递增查询;也允许我们将尾节点当成索引0的节点,头节点L0层的前驱节点当做索引zsl->length-1的节点,索引按分值从大到小递增查询。当我们调用下面的方法按照索引区间来查询时,会把我们的索引转换成跨度,然后查找落在跨度的第一个节点,之后根据reverse(逆向状态)决定是要正向查询还是逆向查询。

假设我们要进行正向查询(即:索引按分值从小到大递增查询),给定的索引区间是[0,2],那么我们要找到跨度为1的节点,然后从跨度为1的节点L0层逐个递进,直到停留在跨度为3的节点,头节点L0层的前驱节点zsl->header.level[0].forward在跳跃表的索引为0,跨度为1,在跨度为1的节点从L0层逐个递进,一直递进到跨度为3的节点,这样便完成了索引区间[0,2]的查询。如果我们要进行逆向查询(即:索引按分值从大到小递增查询),索引区间依旧是[0,2],那么我们要找到跨度为跨度为zsl->length-0=zsl->length的节点,那自然是尾节点,找到跨度区间的第一个节点后,我们通过backward指针逐个后退,一直后退到跨度为zsl->length-2的节点,如此便完成查询。

void zrangeGenericCommand(client *c, int reverse) {
robj *key = c->argv[1];
robj *zobj;
int withscores = 0;
long start;
long end;
long llen;
long rangelen; //读取起始索引和终止索引,如果存在一个索引读取失败,则退出
if ((getLongFromObjectOrReply(c, c->argv[2], &start, NULL) != C_OK) ||
(getLongFromObjectOrReply(c, c->argv[3], &end, NULL) != C_OK))
return;
//判断是否要返回分值
if (c->argc == 5 && !strcasecmp(c->argv[4]->ptr, "withscores")) {
withscores = 1;
} else if (c->argc >= 5) {
addReply(c, shared.syntaxerr);
return;
}
//判断key是否存在,如果不存在则退出,如果存在但类型不为ZSET也退出。
if ((zobj = lookupKeyReadOrReply(c, key, shared.emptyarray)) == NULL
|| checkType(c, zobj, OBJ_ZSET))
return; /*
* Sanitize indexes.
* 审查索引,这里主要针对传入索引为负数的情况,大家都知道,如果一个
* 跳跃表的节点个数为N,我们要从起始节点查询到末尾节点,可以用[0,N-1]
* 或者[0,-1],当传入的end<0时,这里会重新规正end的索引,llen为zset的长度,
* 因此查询[0,-1],这里会规正为[0,N-1]。同理,start也会被规正,如果我们查询
* [-5,-3],即代表查询有序集合倒数第5个节点至倒数第三个节点,前提是N>=5这个
* 查询才有意义。如果我们的起始索引传入的是一个绝对值>N的负数,那么llen + start的
* 结果也为负数,如果判断start<0,则start会被规正为0。
* */
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.emptyarray);
return;
}
//如果判断终止索引大于等于节点数,则规整为llen-1
if (end >= llen) end = llen - 1;
//计算要返回的节点数
rangelen = (end - start) + 1;
//……
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
//压缩列表逻辑……
} 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.
* 这里会根据给定的开始索引,查找该索引对应的节点,并将ln指向该节点。
* 需要注意的一点是:平常我们都认为header.level[0].forward指向的节点,在跳跃表
* 中的索引为0,但有时候跳跃表的末尾节点zsl->tail的索引值也有可能为0,这里就要提到
* 逆向查询。
* 当我们使用ZRANGE key min max [WITHSCORES]命令查询时,成员的位置是按照其分值
* 从小到大来排序,这时候header.level[0].forward的索引值为0,
* header.level[0].forward.level[0].forward的索引值为1。而zls->tail的索引值
* 为zls->length-1。
* 当我们使用ZREVRANGE key start stop [WITHSCORES]命令查询时,成员的位置是按照
* 其分值从大到小来排序,这时候zls->tail的索引值为0,header.level[0].forward的
* 索引值为zls->length-1,header.level[0].forward.level[0].forward的索引值为
* zls->length-2。当reverse为1时,本次查询即为逆向查询。
* 我们注意到不管是if还是else分值,只要start>0,最终都会执行zslGetElementByRank()
* 将ln定位到起始节点。当start为0时,如果是逆向查询,则索引0的位置是尾节点zsl->tail,
* 如果是正向查询,索引0的位置则是zsl->header->level[0].forward。
* 那么(llen - start)和(start + 1)又代表什么含义呢?为什么zslGetElementByRank()
* 可以根据这两个公式的计算结果,定位到索引对应的节点呢?其实这两个公式计算的是跨度,而
* zslGetElementByRank()则是根据给定的跳跃表和跨度查找节点而已。
* 如果是正常查询,假设起始索引为0,则跨度为start(0)+1=1,刚好为头节点L0层到达第一个节点的
* 跨度为1;如果起始索引为1,则跨度为start(1)+1=2,刚好是头节点到达索引值为1的节点的跨度。
* 如果是逆向查询,索引值为0代表尾节点,而llen-start(0)=llen为头节点到达尾节点的跨度;同理,
* 倒数第二个节点的索引值为1,头节点到达倒数第二个节点的跨度为llen-start(1)=llen-1。
* */
if (reverse) {
ln = zsl->tail;
if (start > 0)
ln = zslGetElementByRank(zsl, llen - start);
} else {
ln = zsl->header->level[0].forward;
if (start > 0)
ln = zslGetElementByRank(zsl, start + 1);
}
//定位到起始节点后,根据逆向状态,不为0时后退查询(ln-backward),为0时递进查询(ln->level[0].forward)
while (rangelen--) {
serverAssertWithInfo(c, zobj, ln != NULL);
ele = ln->ele;
if (withscores && c->resp > 2) addReplyArrayLen(c, 2);
addReplyBulkCBuffer(c, ele, sdslen(ele));
if (withscores) addReplyDouble(c, ln->score);
ln = reverse ? ln->backward : ln->level[0].forward;
}
} else {
serverPanic("Unknown sorted set encoding");
}
}

  

查找到跨度对应的节点,查找到跨度对应的节点,则在<1>处返回,如果我们传入的跨度大于头节点到尾节点的跨度,则返回NULL。

/*
* Finds an element by its rank. The rank argument needs to be 1-based.
* */
zskiplistNode *zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
zskiplistNode *x;
unsigned long traversed = 0;//累计跨度
int i; x = zsl->header;
/*
* 从头节点的最高层出发,如果基于当前层能够递进到前一个节点,
* 则把当前节点的跨度加到traversed。
*/
for (i = zsl->level - 1; i >= 0; i--) {
while (x->level[i].forward && (traversed + x->level[i].span) <= rank) {
traversed += x->level[i].span;
x = x->level[i].forward;
}
/*
* 如果累计跨度与调用方传入的跨度相等,则代表x已经前进到调用方
* 所要求达到的跨度的节点,返回x。
*/
if (traversed == rank) {
return x;//<1>
}
}
//如果传入的跨度大于头节点到尾节点的跨度,则返回NULL。
return NULL;
}

  

跳跃表除了可以根据索引区间来查询,还可以根据分值区间来查询,这里我们又见到了结构体zrangespec。当我们需要判断一个节点是否落在我们指定的分值区间内,需要调用zslValueGteMin()和zslValueLteMax(),当传入一个指定的分值和区间,zslValueGteMin()和zslValueLteMax()的结果不为0,则表明节点落在分值区间内。此外,这两个方法还可以判断一个跳跃表是否和区间有交集,比如调用zslValueGteMin()时,传入尾节点(跳跃表分值最大的节点)及一个指定区间,如果尾节点没有落在指定区间,代表此区间都大于尾节点,此时我们不需要遍历跳跃表即可返回一个空数组,告诉客户端在指定区间内找不到任何节点;同理,调用zslValueLteMax()时传入头节点L0层的前驱节点(跳跃表分值最小节点)没有落在区间内,则表明区间小于跳跃表,同样不需要遍历跳跃表即可返回空数组给客户端,告诉客户端在指定区间内找不到任何节点。

/*
* Struct to hold a inclusive/exclusive range spec by score comparison.
* 此结构体用于表示一个指定区间,minex为0时表示在进行最小值比较时,要包含最小值本身
* 同理maxex为0时表示进行最大值比较时,要包含最大值本身。
* 比如:min=2,max=9
* 当minex=0,maxex=0时,区间为:[2,9]
* 当minex=1,maxex=0时,区间为:(2,9]
* 当minex=0,maxex=1时,区间为:[2,9)
* 当minex=1,maxex=1时,区间为:(2,9)
* */
typedef struct {
double min, max;
int minex, maxex; /* are min or max exclusive? */
} zrangespec; /*
* 如果spec->minex不为0,返回分值是否大于区间最小值的比较结果,
* 为0则返回分值是否大于等于区间最小值的比较结果。
* 如果传入一个跳跃表尾节点的分值zsl->tail.score(即:跳跃表最大分值)和区间返回结果为0,
* 则表示跳跃表和区间没有交集。
* 这里分两种情况:
* spec->minex不为0:区间要查询分值大于spec->min的元素,
* zsl->tail.score<=spec->min代表跳跃表最大分值小于等于min,返回结果为0。
* spec->minex为0:区间要查询分值大于等于spec->min的元素,
* zsl->tail.score<spec->min代表跳跃表最大分值小于min,返回结果为0。
*/
int zslValueGteMin(double value, zrangespec *spec) {
return spec->minex ? (value > spec->min) : (value >= spec->min);
} /*
* 如果spec->maxex不为0,返回分值是否小于区间最大值的比较结果,为0则返回分值
* 是否小于等于区间最大值的比较结果。
* 如果传入一个跳跃表头节点L0层指向节点的分值
* zsl->header.level[0].forward.score(即:跳跃表最小分值)和
* 区间返回结果为0,则表示跳跃表和区间没有交集。
* 这里分两种情况:
* spec->maxex不为0:区间要查询分值小于spec->max的元素,
* zsl->header.level[0].forward.score>=spec->max代表跳跃表最小分值大于等于区间
* 最大分值,返回结果为0。
* spec->maxex为0:区间要查询分值小于等于spec->max的元素,
* zsl->header.level[0].forward.score>spec->max代表跳跃表最小分值大于区间最大分值,
* 返回结果为0。
*/
int zslValueLteMax(double value, zrangespec *spec) {
return spec->maxex ? (value < spec->max) : (value <= spec->max);
}

    

在真正根据分值区间查询跳跃表前,会校验区间是否有效,如果我们输入一个区间[a,b],但a>b,那么这个区间肯定是无效区间,无须遍历跳跃表;如果a=b,如果区间的开闭状态出现:(a,b)、(a,b]、[a,b)这三种情况,也是无效区间,只有[a,b]才会去查询节点,表示需要查找分值为a(或者b)的节点。当校验完区间是有效后,还会调用zslValueGteMin()和zslValueLteMax()判断跳跃表和区间是否存在交集,即区间是否整体大于跳跃表或整体小于跳跃表,如果出现这两种情况则表明区间和跳跃表无交集,也就不需要遍历。

/*
* Returns if there is a part of the zset is in range.
* 判断跳跃表和区间是否存在交集
* */
int zslIsInRange(zskiplist *zsl, zrangespec *range) {
zskiplistNode *x; /*
* Test for ranges that will always be empty.
* 校验区间范围是否有效,无效则返回0表示查询结果为空:
* 1.如果最小值大于最大值,则无效。
* 2.如果最小值等于最大值,且区间为:(min,max)、(min,max]、[min,max)则无效。
* */
if (range->min > range->max ||
(range->min == range->max && (range->minex || range->maxex)))
return 0;
/*
* 如果尾节点不为NULL,则把跳跃表最大分值zsl->tail.score与区间比较,
* 如果range->minex不为0,则查询分值大于range->min的元素,如果跳跃表
* 最大分值zsl->tail.score小于等于range->min,则表示跳跃表和区间没有交集,
* 无须遍历跳跃表查询;同理如果range->minex为0,则查询分值大于等于range->min
* 的元素,如果zsl->tail.score小于range->min,则表示跳跃表和区间没有交集,
* 也无须遍历跳跃表查询。
*/
x = zsl->tail;
if (x == NULL || !zslValueGteMin(x->score, range))
return 0;
/*
* 如果头节点L0层的前驱节点不为NULL,则把跳跃表最小分值zsl->header->level[0].forward.score
* 与区间比较,如果range->maxex不为0,则查询分值小于range->maxex的元素,如果跳跃表最小
* 分值zsl->header->level[0].forward.score大于等于range->max,则表示跳跃表和区间没有交集,
* 无须遍历跳跃表查询;同理如果range->maxex为0,则查询分值小于等于range->maxex的元素,如果跳跃表
* 最小分值zsl->header->level[0].forward.score大于range->maxex,则表示跳跃表和区间没有交集,
* 也无须遍历跳跃表查询。
*/
x = zsl->header->level[0].forward;
if (x == NULL || !zslValueLteMax(x->score, range))
return 0;
//跳跃表和区间存在交集,需要遍历跳跃表查询。
return 1;
}

  

跳跃表允许我们根据分值区间进行正向查询(分值从小到大)或逆向查询(分值从大到小),如果是正向查询,则调用zslFirstInRange()方法,先判断跳跃表和指定区间是否存在交集,如果存在则查找指定区间内的分值最小的节点并返回。

/*
* Find the first node that is contained in the specified range.
* Returns NULL when no element is contained in the range.
* 查找落在指定区间的第一个节点,如果没有元素落在这个区间则返回NULL。
* */
zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) {
zskiplistNode *x;
int i; /*
* If everything is out of range, return early.
* 如果跳跃表和区间没有交集则无须遍历,直接返回NULL。
* */
if (!zslIsInRange(zsl, range)) return NULL;
//从头节点的最高层开始遍历
x = zsl->header;
for (i = zsl->level - 1; i >= 0; i--) {
/*
* Go forward while *OUT* of range.
* 如果x->level[i].forward不为NULL,根据其分值x->level[i].forward->score和
* range->minex判断前驱节点是否能前进。
* 这里分两种情况:
* range->minex不为0:判断前驱节点的分值是否大于range->min,如果小于等于的话
* expression=zslValueGteMin(x->level[i].forward->score, range)为0,
* 代表需要前进,找到大于min的节点,而!expression为1,while条件成立,x会
* 前进到它的前驱节点。当x的前驱节点的分值大于min,就会停止循环,x会停留在区间内
* 第一个节点的后继节点。
* range->minex为0:判断前驱节点的分值是否大于等于range->min,如果小于的话,
* expression=zslValueGteMin(x->level[i].forward->score, range),expression为0,
* 需要前进,找到大于等于min的节点,而!expression为1,while条件成立,x会
* 前进到它的前驱节点。当x的前驱节点的分值大于等于min,就会停止循环,x会停留在区间内
* 第一个节点的后继节点。
* */
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停留在区间内第一个节点的后继节点,为了达到区间内的
* 第一个节点,x要在L0层前进到它的前驱节点。
* */
x = x->level[0].forward;
serverAssert(x != NULL); /*
* Check if score <= max.
* range->maxex不为0:如果区间内第一个节点的分值大于等于spec->max,
* expression=zslValueLteMax(x->score, range),expression结果为0
* !expression为1,表示查询异常,返回NULL。
* range->maxex为0:如果区间内第一个节点的分值大于spec->max,
* 则expression=zslValueLteMax(x->score, range),expression结果为0
* !expression为1,表示查询异常,返回NULL。
* */
if (!zslValueLteMax(x->score, range)) return NULL;
return x;
}

  

如果要进行逆向查询,则调用zslLastInRange(),这里同样先判断跳跃表是否和区间存在交集,只有存在交集才会进行下一步的判断,查找指定区间内分值最大的节点并返回。

/*
* Find the last node that is contained in the specified range.
* Returns NULL when no element is contained in the range.
* 查找落在指定区间的最后一个节点,如果没有元素落在这个区间则返回NULL。
* */
zskiplistNode *zslLastInRange(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 *IN* range.
* 根据区间range和前驱节点的分值判断是否前进,如果x->level[i].forward
* 不为NULL,根据range->maxex判断前驱节点是否能前进。
* 这里分两种情况:
* 如果range->maxex不为0,且前驱节点的分值小于range->max,则可以前进。
* 如果range->maxex为0,且前驱节点的分值小于等于range->max,则可以前进。
* */
while (x->level[i].forward &&
zslValueLteMax(x->level[i].forward->score, range))
x = x->level[i].forward;
} /* This is an inner range, so this node cannot be NULL. */
serverAssert(x != NULL); /*
* Check if score >= min.
* 如果range->minex不为0,x的分值小于或等于range->min,代表查询出现异常,则返回NULL。
* 如果range->minex为0,x的分值小于range->min,代表查询出现异常,则返回NULL。
* */
if (!zslValueGteMin(x->score, range)) return NULL;
return x;
}

  

在了解完上面的内容后,下面我们要步入正题:如何根据分值区间进行正向或逆向查找节点。在下面代码<1>处,会根据逆向状态选择ln是指向区间分值最大的节点,或是分值最小的节点。在定位到起始节点后,会在<2>处的while循环对节点进行偏移,如果到达偏移位置后的ln不为NULL,则会进入<3>处的while循环,查找分值落在区间内的节点,这里会根据逆向状态是否不为0,决定是用backward指针后退,还是向L0层的前驱节点递进,一直到分值不落在区间内跳出while循环,或者ln为NULL,又或者limit为0结束while循环。如果我们没有指定偏移(offset)和返回数量(limit),则不会进行偏移,limit默认值为-1,limit--永远不为0,这里会返回落在区间内的所有节点,能结束while循环只有遇到分值不落在区间内的节点,或者是ln为NULL。

/* This command implements ZRANGEBYSCORE, ZREVRANGEBYSCORE. */
void genericZrangebyscoreCommand(client *c, int reverse) {
zrangespec range;//指定区间
robj *key = c->argv[1];
robj *zobj;
long offset = 0, limit = -1;//偏移和结果返回数量
int withscores = 0;
unsigned long rangelen = 0;
void *replylen = NULL;
int minidx, maxidx; /*
* Parse the range arguments.
* 解析范围参数
* ZRANGEBYSCORE key min max和ZREVRANGEBYSCORE key max min两个命令
* 都是此函数实现的,如果客户端输入的命令为ZRANGEBYSCORE,则reverse为0,按
* 从小到大查找分值及元素,分值小的在前,分值大的在后。如果客户端输入的命令为
* ZREVRANGEBYSCORE,则reverse不为0,按从大到小查找分值及元素,分值大的在前,
* 分值小的在后。
*
* */
if (reverse) {
/* Range is given as [max,min] */
maxidx = 2;
minidx = 3;
} else {
/* Range is given as [min,max] */
minidx = 2;
maxidx = 3;
} if (zslParseRange(c->argv[minidx], c->argv[maxidx], &range) != C_OK) {
addReplyError(c, "min or max is not a float");
return;
} /*
* Parse optional extra arguments. Note that ZCOUNT will exactly have
* 4 arguments, so we'll never enter the following code path.
* 遍历可选参数,这里会判断是否要返回分值(withscores),是否要对查询结果进行偏移(offset)和数量(limit)的限制
* */
if (c->argc > 4) {
int remaining = c->argc - 4;
int pos = 4; while (remaining) {
if (remaining >= 1 && !strcasecmp(c->argv[pos]->ptr, "withscores")) {
pos++;
remaining--;
withscores = 1;
} else if (remaining >= 3 && !strcasecmp(c->argv[pos]->ptr, "limit")) {
if ((getLongFromObjectOrReply(c, c->argv[pos + 1], &offset, NULL)
!= C_OK) ||
(getLongFromObjectOrReply(c, c->argv[pos + 2], &limit, NULL)
!= C_OK)) {
return;
}
pos += 3;
remaining -= 3;
} else {
addReply(c, shared.syntaxerr);
return;
}
}
} /*
* Ok, lookup the key and get the range
* 如果key所对应的zobj不存在,或者zobj的类型不为zset,则退出。
* */
if ((zobj = lookupKeyReadOrReply(c, key, shared.emptyarray)) == NULL ||
checkType(c, zobj, OBJ_ZSET))
return; if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
//压缩列表流程...
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {//zobj类型为跳跃表
zset *zs = zobj->ptr;
zskiplist *zsl = zs->zsl;
zskiplistNode *ln; /*
* If reversed, get the last node in range as starting point.
* 如果是逆向查询,ln会指向区间分值最大的节点,如果是正向查询,ln则指向区间分值最小的节点。
* */
if (reverse) {
ln = zslLastInRange(zsl, &range);
} else {
ln = zslFirstInRange(zsl, &range);
} /*
* No "first" element in the specified interval.
* 如果没有落在区间的开始节点则退出
* */
if (ln == NULL) {
addReply(c, shared.emptyarray);
return;
} /* We don't know in advance how many matching elements there are in the
* list, so we push this object that will represent the multi-bulk
* length in the output buffer, and will "fix" it later */
replylen = addReplyDeferredLen(c); /*
* If there is an offset, just traverse the number of elements without
* checking the score because that is done in the next loop.
* <2>如果有偏移量,则根据reverse状态选择是后退还是递进,以达到偏移量。
* 如果客户端有传入偏移,则offset不为0,这里会循环到offset为0或ln为NULL时跳出循环。
* 否则offset默认值为0,不会进入此循环。
* */
while (ln && offset--) {
if (reverse) {
ln = ln->backward;
} else {
ln = ln->level[0].forward;
}
}
/*
* <3>如果客户端有传入偏移和数量,则limit不为0,此时会根据reverse状态后退或者前进至
* ln为NULL或者limit为0,否则limit默认值为-1,limit--永远为true(注:只要limit
* 不为0,则永远为true,即便是负数),这里就会循环到ln为NULL时,获取所有分值符合区间
* 节点的。
* 除了limit为0,或者ln为NULL会跳出while循环,在<4>处还会根据reverse状态判断分值是否在区间,
* 如果不在则跳出循环,如果分值符合区间,还会在<5>处根据reverse状态选择是后退到后一个节点(ln->backward),
* 还是前进到前一个节点(ln->level[0].forward)。
*/
while (ln && limit--) {
/*Abort when the node is no longer in range.*/
if (reverse) {//<4>
if (!zslValueGteMin(ln->score, &range)) break;
} else {
if (!zslValueLteMax(ln->score, &range)) break;
} rangelen++;
if (withscores && c->resp > 2) addReplyArrayLen(c, 2);
addReplyBulkCBuffer(c, ln->ele, sdslen(ln->ele));
if (withscores) addReplyDouble(c, ln->score); /* Move to next node */
if (reverse) {//<5>
ln = ln->backward;
} else {
ln = ln->level[0].forward;
}
}
} else {
serverPanic("Unknown sorted set encoding");
} if (withscores && c->resp == 2) rangelen *= 2;
setDeferredArrayLen(c, replylen, rangelen);
}

  

最后,我们要了解如何获取一个元素在跳跃表中的索引,其实这里面的逻辑也是非常的简单,我们先从字典上获取节点的分值,然后根据分值及元素获取其在跳跃表中的索引,这里依旧支持正向查询或逆向查询,如果是正向查询,分值越小,索引越小,如果分值相等,则元素越小,索引越小;如果是逆向查询,则分值越大,索引越小,如果分值相等,则元素越大,索引越小。

/* Given a sorted set object returns the 0-based rank of the object or
* -1 if the object does not exist.
* 返回元素在有序集合中的索引,如果返回-1则代表元素不在有序集合内。
*
* For rank we mean the position of the element in the sorted collection
* of elements. So the first element has rank 0, the second rank 1, and so
* forth up to length-1 elements.
* 在跳跃表中第一个元素的索引为0,第二个元素索引为1,以此类推,最后一个元素索引为length-1。
*
* If 'reverse' is false, the rank is returned considering as first element
* the one with the lowest score. Otherwise if 'reverse' is non-zero
* the rank is computed considering as element with rank 0 the one with
* the highest score.
* 如果reverse为0,跳跃表索引从分值最小的节点开始,即zsl->header.level[0].forward索引为0、
* zsl->header.level[0].forward.level[0].forward索引为1,zsl->tail索引为zsl-length-1;
* 如果reverse不为0,跳跃表索引从分值最大的节点开始,即zsl->tail索引为0,zsl->tail.backward索引
* 为1,zsl->header.level[0].forward索引为zsl->length-1
* */
long zsetRank(robj *zobj, sds ele, int reverse) {
unsigned long llen;
unsigned long rank; llen = zsetLength(zobj); if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
//压缩列表逻辑……
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplist *zsl = zs->zsl;
dictEntry *de;
double score; de = dictFind(zs->dict, ele);
if (de != NULL) {
/*
* 如果元素存在在跳跃表上,则获取元素的分支,并根据
* 分支判断其在跳跃表中的跨度,根据跨度计算节点在跳跃表
* 中的索引。
* 如果是正向查询(reverse为0),则索引为跨度(rank)-1。
* 如果是逆向查询(reverse不为0),则索引为跳跃表长度(zsl->length)-跨度(rank)。
*/
score = *(double *) dictGetVal(de);
rank = zslGetRank(zsl, score, ele);
/* Existing elements always have a rank. */
serverAssert(rank != 0);
if (reverse)
return llen - rank;
else
return rank - 1;
} else {//如果元素不在跳跃表上,则返回-1
return -1;
}
} else {
serverPanic("Unknown sorted set encoding");
}
}

  

获取完分值后,需要定位节点在跳跃表中的跨度,然后根据逆向状态及跨度,计算节点在跳跃表中的索引。

/* 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.
* 根据给定的分支和元素查找其节点在跳跃表中的跨度,返回0代表节点不存在。
* */
unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
zskiplistNode *x;
unsigned long rank = 0;
int i;
//从头节点最高层遍历,如果能前进到前一个节点,则把当前节点的跨度加到rank上
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
* x可能停留在头节点,此处判断是保证节点的元素不为NULL。
* */
if (x->ele && sdscmp(x->ele, ele) == 0) {
return rank;
}
}
return 0;
}

  

至此,笔者和大家一起学习了Redis跳跃表的是如何插入节点、删除节点、更新节点以及如何对跳跃表中的节点进行不同维度(索引、分值)的查询。跳跃表是一种应用相当广泛的数据结构,很多场景下人们都用跳跃表代替B-Tree,因为跳跃表和B-Tree有着一样的查询时间复杂度O(logN),但跳跃表的实现却比B-Tree简单很多。而Redis正是借助了跳跃表的思路实现了有序集合,使得很多需要存储、排序海量数据的业务得以实现,如:微博热搜或者头条新闻,都可以使用Redis有序集合来解决。

当然,由于笔者的时间精力有限,这里并没有完全介绍所有跳跃表命令的相关实现,但笔者相信能看到这里的人,基本已经掌握了跳跃表的整体脉络。如果对跳跃表其余命令有兴趣的朋友,可以自行翻阅Redis源码,或者评论私信笔者你们的疑问,如果问题多的话笔者还会针对大家共同的问题进行讲解。

  

Redis源码解析之跳跃表(三)的更多相关文章

  1. Redis源码解析之跳跃表(一)

    跳跃表(skiplist) 有序集合(sorted set)是Redis中较为重要的一种数据结构,从名字上来看,我们可以知道它相比一般的集合多了一个有序.Redis的有序集合会要求我们给定一个分值(s ...

  2. TiKV 源码解析系列文章(三)Prometheus(上)

    本文为 TiKV 源码解析系列的第三篇,继续为大家介绍 TiKV 依赖的周边库 rust-prometheus,本篇主要介绍基础知识以及最基本的几个指标的内部工作机制,下篇会介绍一些高级功能的实现原理 ...

  3. .Net Core缓存组件(Redis)源码解析

    上一篇文章已经介绍了MemoryCache,MemoryCache存储的数据类型是Object,也说了Redis支持五中数据类型的存储,但是微软的Redis缓存组件只实现了Hash类型的存储.在分析源 ...

  4. Redis源码解析:15Resis主从复制之从节点流程

    Redis的主从复制功能,可以实现Redis实例的高可用,避免单个Redis 服务器的单点故障,并且可以实现负载均衡. 一:主从复制过程 Redis的复制功能分为同步(sync)和命令传播(comma ...

  5. Spring源码解析之BeanFactoryPostProcessor(三)

    在上一章中笔者介绍了refresh()的<1>处是如何获取beanFactory对象,下面我们要来学习refresh()方法的<2>处是如何调用invokeBeanFactor ...

  6. Java源码解析——集合框架(三)——Vector

    Vector源码解析 首先说一下Vector和ArrayList的区别: (1) Vector的所有方法都是有synchronized关键字的,即每一个方法都是同步的,所以在使用起来效率会非常低,但是 ...

  7. Redis源码解析:13Redis中的事件驱动机制

    Redis中,处理网络IO时,采用的是事件驱动机制.但它没有使用libevent或者libev这样的库,而是自己实现了一个非常简单明了的事件驱动库ae_event,主要代码仅仅400行左右. 没有选择 ...

  8. Redis源码解析:05跳跃表

    一:基本概念 跳跃表是一种随机化的数据结构,在查找.插入和删除这些字典操作上,其效率可比拟于平衡二叉树(如红黑树),大多数操作只需要O(log n)平均时间,但它的代码以及原理更简单.跳跃表的定义如下 ...

  9. Redis源码解析:26集群(二)键的分配与迁移

    Redis集群通过分片的方式来保存数据库中的键值对:一个集群中,每个键都通过哈希函数映射到一个槽位,整个集群共分16384个槽位,集群中每个主节点负责其中的一部分槽位. 当数据库中的16384个槽位都 ...

随机推荐

  1. layUI form表单 防止多次点击重复提交

    //监听 弹框-变更处理备注-提交 form.on('submit(popFormSubPass)', function (data) { //防止重复点击: 单击之后提交按钮不可选,防止重复提交 v ...

  2. Django(7)url命名的作用

    前言 为什么我们url需要命名呢?url命名的作用是什么?我们先来看一个案例 案例 我们先在一个Django项目中,创建2个App,前台front和后台cms,然后在各自app下创建urls.py文件 ...

  3. Codeforces Round #661 (Div. 3)

    A. Remove Smallest 题意:数组是否满足任意i,j保证|ai-aj|<=1,如果都可以满足,输出YES,否则输出NO 思路:直接排序遍历即可 代码: 1 #include< ...

  4. centos7安装es6.4.0

    一.首先进入到opt文件夹cd opt二.然后下载es安装包wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearc ...

  5. CRM帮助B2B企业持续改善战略决策「上篇」

    数据一直都是企业和客户的热点话题.客户期望得到更加个性化的感受,企业则期望使用数据来持续改善战略决策和给予更好的服务 B2B企业如何更合理地利用客户资料: 数据采集 长期以来,B2C行业的企业都是通过 ...

  6. 把一个整体目标设置成多个分阶段目标,完成了一个目标后,就相当于一件事OVER

    如果事情有变坏的可能,不管这种可能性有多小,它总会发生 . 一.任何事都没有表面看起来那么简单:二.所有的事都会比你预计的时间长:三.会出错的事总会出错:四.如果你担心某种情况发生,那么它就一定会发生 ...

  7. lambda,filter,map,reduce

    # lambda,filter,map,reduce from functools import reduce print('返回一个迭代器') print((x) for x in range(5) ...

  8. 。 (有些情况下通过 lsof(8) 或 fuser(1) 可以 找到有关使用该设备的进程的有用信息)

    umount时目标忙解决办法 标签(空格分隔): ceph ceph运维 osd 在删除osd后umount时,始终无法umonut,可以通过fuser查看设备被哪个进程占用,之后杀死进程,就可以顺利 ...

  9. win10家庭版升级 到win10企业版

    成功升级3小时  20200124 拿到电脑 win10家庭版 不会用 找admin都找不到只能用企业版 升级win10家庭版 到win10企业版 在msdn下载win10企业版iso iso 文件管 ...

  10. 7.json&pickle及软件目录结构规范

    json(可以序列化简单数据类型,用于不同语言之间的数据交换传输)import jsonjson.dumps() 写入json.loads() 读取json.dump(info,f) == f.wri ...