• Redis源码解析之跳跃表(三)


    我们再来学习如何从跳跃表中查询数据,跳跃表本质上是一个链表,但它允许我们像数组一样定位某个索引区间内的节点,并且与数组不同的是,跳跃表允许我们将头节点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源码,或者评论私信笔者你们的疑问,如果问题多的话笔者还会针对大家共同的问题进行讲解。

      

  • 相关阅读:
    Canvas
    FastDFS 下载与安装
    Oracle 表空间错误集锦
    Sql Server 网络配置
    telnet 功能启用并测试端口是否正常
    UIBarButtonItem 上按钮切换/隐藏的简单例子 转载
    UIScrollView详解
    warning:performSelector may cause a leak because its selector is unknown
    dispatch_once优化代码性能 转
    UIView 和 CALayer的那点事
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/14883192.html
Copyright © 2020-2023  润新知