Skiplist 学校是不教的,《算法导论》上也没有,所以有一个印象深刻的图来解释似乎更合适。

image

有一个视频大致介绍了跳表(虽然实现上和redis的跳表不一样,但是至少是比较像的)

https://www.bilibili.com/video/av930970170/

Redis 的跳表一共涉及到以下的结构体。因为它只支持 sds 类型字符串,以及 double 类型的 score,所以它不是泛型的。

zskiplistNode 中,backward 可以简单理解为链表中的 prev 指针,而 level 表示的就是上图的每个节点的每个 L0, L1, L2... 故而 forward 简单理解为每一层的 next 指针,而 span 表示的就是它到下一个节点的距离(如上图节点 2 的 L1 的 span 就是 2)。如果 forward == NULL,那就是其后面还有几个节点(如上图节点 13 L3 的 span 就是 5)。如果一个 skiplist 只有 level[0],那么该跳表就相当于普通链表了。

zskiplist 中,length 就是跳表一共有多少节点,level 就是当前跳表最高有几层。

typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

Redis 为什么选择了跳表而不是某种平衡树?以下是作者 antirez 所说。简单总结就是(结合源代码的意译),首先 skiplist 不是很占内存,而且占多少内存你可以手动调整 zslRandomLevel(void),其次对链表范围查找,跳表至少会和其他平衡树表现一样好,已排序集合而言常常要做很多范围查找,最后它实现简单,debug 容易。

https://news.ycombinator.com/item?id=1171423

image

跳表创建

跳表创建会生成一个有 32 层的头节点(ZSKIPLIST_MAXLEVEL 是 32,不论怎么样也不会超过 32 层),但是 32 层中暂时只有 level[0] 是有效的。默认 32 层是为了避免对头节点 realloc。

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;
    }
    zsl->header->backward = NULL;
    zsl->tail = NULL;
    return zsl;
}

跳表插入值

插入值的代码略长,个人觉得与其解释其代码,还不如形象具体地解释其中的局部变量的作用。

zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; // update 记录所有需要更新的节点
    unsigned long rank[ZSKIPLIST_MAXLEVEL]; // rank 到达需要更新的节点,有多少节点
    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 || // 找比 score/element 大的
                    (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;
    }
    /* 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 不可能小于等于 0
    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;
        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;
    }

    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }

    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;
}

update 是一个指针数组,记录的是插入值后需要更新的所有节点。rank[i] 则是到达 update[i] 所需要经过的节点数量,可以简单理解为 rank[i]update[i] 节点的下标。这种解释是不形象具体的,所以下面是更加好一点的解释。

为了简化绘图,每一个节点都会按照右边的方式绘制,左边是它原来的样子。

image

下图是初始化后(zslCreate)的结果:

image

对下面的 skiplist 插入 score = 4, ele = 4, 高度为 1:

image

毫无疑问,新插入的节点必然是在 3 之后的,你站在 3 之后观察,会发现第 0 层能够看到的第一个节点是 3,第 0 层被 3 挡住的有三个节点,所以 update[0] 是节点 3,rank[0] 是 3。

image

同样的,第一层能够看到的是节点 3,被挡住的有 3 个节点。所以 update[1] 是节点 3,rank[1] 是 3。

image

同样的,第二层能够看到的是节点 1,被挡住的有 1 个节点。所以 update[2] 是节点 1,rank[2] 是 1。

image

现在放置创建的节点 (4, 4),利用记录的 rank,更新各个 update 的 span 和 forward。rank[0] 的另一个意思是update[i]前方一共有几个节点,相当于记录一个链表中某个节点前方有多少个节点。所以 rank[i] - rank[0] 这种做法相当于对前缀和求某个区间的和。而 + 1 的原因纯粹就是因为是新插入节点所以需要 + 1。所以节点 1 L2 更新 span 为 3,3 的 L1、L0 更新为 1。其他的不过是常见链表的操作罢了。

image

跳表节点删除

删除依然需要用上刚刚说的 update。因为是移除,各层只要减一即可(这是 zslDeleteNode 做的事情)。所以依然是边找节点边构造 update,然后删除。

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 */
}

Range 操作

range 操作就是 zrange 一系列指令。

ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count]  [WITHSCORES]

在没有指定 byscore 和 bylex 的时候,就是按照下标查找的(byrank)。据官方文档说,bylex 需要确保所有的 score 都是相同的,但实际似乎只要保证 score 和 ele 同步递增就行了——因为其实现就是这样的。

如果是按顺序返回,它们都首先执行找到第一个符合条件的节点,然后由调用它们的函数,遍历节点,把结果写入 handler。逆序类似。如:

        if (reverse) {
            ln = zslNthInRange(zsl,range,-offset-1);
        } else {
            ln = zslNthInRange(zsl,range,offset);
        }

        while (ln && limit--) {
            /* Abort when the node is no longer in range. */
            if (reverse) {
                if (!zslValueGteMin(ln->score,range)) break;
            } else {
                if (!zslValueLteMax(ln->score,range)) break;
            }

            rangelen++;
            handler->emitResultFromCBuffer(handler, ln->ele, sdslen(ln->ele), ln->score);

            /* Move to next node */
            if (reverse) {
                ln = ln->backward;
            } else {
                ln = ln->level[0].forward;
            }
        }

byrank

byrank 就是前文插入操作计算 rank 的步骤。唯一不同就是它不需要比较 score 和 ele。

zskiplistNode *zslGetElementByRankFromNode(zskiplistNode *start_node, int start_level, unsigned long rank) {
    zskiplistNode *x;
    unsigned long traversed = 0;
    int i;

    x = start_node;
    for (i = start_level; i >= 0; i--) {
        while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
        {
            traversed += x->level[i].span;
            x = x->level[i].forward;
        }
        if (traversed == rank) {
            return x;
        }
    }
    return NULL;
}

至于 reverse,不过是 len - 需要找到的 rank 罢了

void genericZrangebyrankCommand(zrange_result_handler *handler,
    robj *zobj, long start, long end, int withscores, int reverse) {

...
                ln = zslGetElementByRank(zsl,llen-start);
...
                ln = zslGetElementByRank(zsl,start+1);
...

byscore

byscore 依然类似于前面的查找。找到第一个大于 start 的节点,然后遍历到 stop。处理 offset 比较简单,故而省略。

zskiplistNode *zslNthInRange(zskiplist *zsl, zrangespec *range, long n) {
... // 找最高层第一个大于 start 的。之所以单独遍历最高层,是为了处理 offset。
    while (x->level[i].forward && !zslValueGteMin(x->level[i].forward->score, range)) {
        edge_rank += x->level[i].span;
        x = x->level[i].forward;
    }
    /* Remember the last node which has zsl->level-1 levels and its rank. */
    last_highest_level_node = x;
    last_highest_level_rank = edge_rank;

    if (n >= 0) {
        // 找范围指定的第一个
        for (i = zsl->level - 2; i >= 0; i--) {
            /* Go forward while *OUT* of range. */
            while (x->level[i].forward && !zslValueGteMin(x->level[i].forward->score, range)) {
                /* Count the rank of the last element smaller than the range. */
                edge_rank += x->level[i].span;
                x = x->level[i].forward;
            }
        }
        ... // 处理 offset
    } else {
        // 找范围指定的最后一个
        for (i = zsl->level - 1; i >= 0; i--) {
            /* Go forward while *IN* range. */
            while (x->level[i].forward && zslValueLteMax(x->level[i].forward->score, range)) {
                /* Count the rank of the last element in range. */
                edge_rank += x->level[i].span;
                x = x->level[i].forward;
            }
        }
        ... // 处理 offset
    }

bylex

bylex,就是按照字典序排序。前文说官方文档要求 score 必须一样。简单解释就是查找的方式依然是匹配第一个可以匹配的,然后以此为基点继续找第一个未匹配的。所以就无法处理多个 score 的情况。但是 score 和 ele 同步递增的情况是可以的。因为 ele 随着 score 排序而变得有顺序。

zskiplistNode *zslNthInLexRange(zskiplist *zsl, zlexrangespec *range, long n) {
... // 几乎和 byscore 一样
    while (x->level[i].forward && !zslLexValueGteMin(x->level[i].forward->ele, range)) {
        edge_rank += x->level[i].span;
        x = x->level[i].forward;
    }
    /* Remember the last node which has zsl->level-1 levels and its rank. */
    last_highest_level_node = x;
    last_highest_level_rank = edge_rank;
...
    if (n >= 0) {
        for (i = zsl->level - 2; i >= 0; i--) {
            /* Go forward while *OUT* of range. */
            while (x->level[i].forward && !zslLexValueGteMin(x->level[i].forward->ele, range)) {
                /* Count the rank of the last element smaller than the range. */
                edge_rank += x->level[i].span;
                x = x->level[i].forward;
            }
        }
... // 处理 offset
    } else {
        for (i = zsl->level - 1; i >= 0; i--) {
            /* Go forward while *IN* range. */
            while (x->level[i].forward && zslLexValueLteMax(x->level[i].forward->ele, range)) {
                /* Count the rank of the last element in range. */
                edge_rank += x->level[i].span;
                x = x->level[i].forward;
            }
        }
... // 处理 offset
}

range delete

三种 range delete 几乎是一样的。

unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, dict *dict) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned long traversed = 0, removed = 0;
    int i;

    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward && (traversed + x->level[i].span) < start) {
            traversed += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }

    traversed++;
    x = x->level[0].forward;
    while (x && traversed <= end) {
        zskiplistNode *next = x->level[0].forward;
        zslDeleteNode(zsl,x,update);
        dictDelete(dict,x->ele);
        zslFreeNode(x);
        removed++;
        traversed++;
        x = next;
    }
    return removed;

跳表节点更新

更新操作没什么特别的,先找到要更新的节点,删除它,再插入新值。

zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {
... // 几乎与插入一样的查找,记录 update。
    /* Jump to our element: note that this function assumes that the
     * element with the matching score exists. */
    x = x->level[0].forward;
    serverAssert(x && curscore == x->score && sdscmp(x->ele,ele) == 0);

    /* If the node, after the score update, would be still exactly
     * at the same position, we can just update the score without
     * actually removing and re-inserting the element in the skiplist. */
    if ((x->backward == NULL || x->backward->score < newscore) &&
        (x->level[0].forward == NULL || x->level[0].forward->score > newscore))
    {
        x->score = newscore;
        return x;
    }

    /* No way to reuse the old node: we need to remove and insert a new
     * one at a different place. */
    zslDeleteNode(zsl, x, update);
    zskiplistNode *newnode = zslInsert(zsl,newscore,x->ele);
    /* We reused the old node x->ele SDS string, free the node now
     * since zslInsert created a new one. */
    x->ele = NULL;
    zslFreeNode(x);
    return newnode;