redis的有序集合你们知道底层是咋实现的吗?看完这篇就透透的了

1,074 阅读4分钟

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

自我介绍

xdm好 我是跳跃表,一种有序的数据结构,在我的每个节点都有一个数组维护了多个可以指向其他节点的指针可以快速访问到这些节点,这也就是为啥我是跳跃表的原因。

我的平均时间复杂度是O(logN) 最坏的话是O(N),在大多数情况下我可以和平衡树的效率相比,但我的实现要比他简单的多。

在redis有哪些使用场景

我是redis中的有序集合的底层实现之一(还有一种是我们前面说的字典),不过天下没有免费的午餐,他用我作为底层实现是有条件的。当元素比较多或者有序集合的成员是比较长的字符串时候就会适用我来实现了。我在redis中的使用场景还是比较少的,一共只有两个地方用到了我,一个就是这个有序集合 还有一个是在集群节点中作内部数据结构使用。

我的实现

我是这个样子的

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

可以看到我的结构首先有两个指针 一个 header 一个tail 分别指向我的头节点和尾节点,还有一个length 记录我的节点数 level 记录的是我所有节点中层高的最大值。我的表头节点是一个很特殊的节点 他是没有存放数据的只是存放了指向下一个同层级的节点的指针和对应的跨度。

下面说下我的节点,他是这个样子的

typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;
  • ele是存放的我们的key值 
  • score是放的我们有序集合的分值,排序就是按照这个字段来排序的 
  • backward是一个指向前一个节点的指针,方便我们从后往前查找 
  • level数组 我们可以看到他的结构中有一个forward指针 这个是指向下一个节点的指针(参考上图),还有一个span是指的跨度(这个指针指向的节点和当前节点中间有几个节点)

来都来了不看看源码吗?

我的操作有很多,比如zslCreate zslFree zslInsert zslDelete等等,我就先讲一下我的

zslInsert的实现吧!xdm先大概过一下代码。

/* Insert a new node in the skiplist. Assumes the element does not already
 * exist (up to the caller to enforce that). The skiplist takes ownership
 * of the passed SDS string 'ele'. */
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    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;
    }
    /* 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();
    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;
}

其实可以看到主要是这块的逻辑:

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

这个地方的逻辑是就是去找我们的插入节点应该在的位置,这块的逻辑有点绕,他是从头节点的level数组从后往前先找到一个score 小于当前要插入的节点的score,然后从这个节点在往后执行while中的代码 最后找到我们的x 我们要插入的节点就是在我们的x后边。

xdm可以重点理解一下这个插入过程 还是很有意思的(有疑惑的地方可以评论区讨论下 必回),通过这个插入方法我们也可以发现redis中的跳跃表是一个升序存储的结构。

xdm通过源码我们可以发现当分数相同的时候跳跃表是怎么进行排序的?

sdscmp(x->level[i].forward->ele,ele) < 0) 对的就是对他们的sds对象进行一个比较,代码给xdm贴出来了

int sdscmp(const sds s1, const sds s2) {
    size_t l1, l2, minlen;
    int cmp;

    l1 = sdslen(s1);
    l2 = sdslen(s2);
    minlen = (l1 < l2) ? l1 : l2;
    cmp = memcmp(s1,s2,minlen);
    if (cmp == 0) return l1>l2? 1: (l1<l2? -1: 0);
    return cmp;
}

还有一个概念是跃表的层高是1-64(在3.2之前是32)之间的数字有图有真相

、跳

最后 xdm码字不易,如果看完有些帮助就给个赞吧~我要去输液了 想你的夜!