Redis-跳跃列表原理源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zanpengfei/article/details/86518799

1、Redis 的 zset 是一个复合结构,结构特点:

1)需要通过hash 结构来存储 value 和 score 的对应关系

2)要支持按照 score 来排序

3)还要按照指定 score 的范围来获取 value 列表的

4)要实现上诉的功能的结构,作者想到了跳跃列表

5)zset的结构:通过一个hash字典加跳跃列表skiplist

 

2、跳跃列表结构

1)基本结构

解释:Redis的跳跃列表总共有64层,上图中每个kv对应的结构如下图中的zslnode结构,kv header也是zslnode结构,只不过value字段是null,score值是Double.MIN_VALUE,相邻来个kv之间用指针窜接起来,形成双向列表,而且是有序的,从小到大,每一层元素的遍历是从kv header开始

2)结构体

struct zsl {

  zslnode* header; // 跳跃列表头指针

  int maxLevel; // 跳跃列表当前的最高层

  map<string, zslnode*> ht; // hash 结构的所有键值对

}

struct zslnode {

  string value;

  double score;

  zslnode*[] forwards;  // 多层连接指针

  zslnode* backward;  // 回溯指针

}

2、查找

1)如果要定位到紫色的模块,首先从header往下定位,比header降一层,在定位比上层降一层的,通过3次定位,最后定位到紫色的那个元素,我们将中间一系列节点称为搜索路径

2)当插入一个新节点时,我们通过随机算法来定位新插入的节点有多少层

3、随机层数

最理想的是每一层的晋升概率都是50%

/* Returns a random level for the new skiplist node we are going to create.

 * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL

 * (both inclusive), with a powerlaw-alike distribution where higher

 * levels are less likely to be returned. */

int zslRandomLevel(void) {

    int level = 1;

    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))

        level += 1;

    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;

 

但是Redis的标准源码的晋升概率只有25%

4、插入过程

/* 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];

        // 如果score相等,还需要比较value

        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;

}

解析:首先我们在搜索合适插入点的过程中将「搜索路径」摸出来了,然后就创建新节点,创建的时候需要给这个节点随机分配一个层数,再将搜索路径上的节点和这个新节点通过前向后向指针串起来。如果分配的新节点的高度高于当前跳跃列表的最大高度,就需要更新一下跳跃列表的最大高度。

5、删除过程

首页找到搜索路径,然后对每一层相关节点排下向前向后指针,最后更新最高层maxLevel

 

6、更新过层

1)当我们调用zadd方法时,如果value不存在,则就是新增

2)如果value存在就是更新

3)如果该score值没有改变排序,那么位置就不需要调整,直接修改元素的score值

4)如果排序位置改变了,那就要调整位置了

5)调整位置源码如下

/* Remove and re-insert when score changes. */

    if (score != curscore) {

        zskiplistNode *node;

        serverAssert(zslDelete(zs->zsl,curscore,ele,&node));

        znode = zslInsert(zs->zsl,score,node->ele);

        /* We reused the node->ele SDS string, free the node now

        * since zslInsert created a new one. */

        node->ele = NULL;

        zslFreeNode(node);

        /* Note that we did not removed the original element from

        * the hash table representing the sorted set, so we just

        * update the score. */

        dictGetVal(de) = &znode->score; /* Update score ptr. */

        *flags |= ZADD_UPDATED;

        }

    return 1;

解析:通过上诉源码可以看出,更新时候,先找到value对应的score,然后删除,删除后在插入这个新的节点。

 

6、如果score值时相同处理逻辑

如果zset中的score值都相同,zset的排序首页比较score值,如果相同则比较value值。

 

7、元素排名(rank)规则

     Redis的skiplist的forward指针上增加了span属性,表示从前一个节点沿着当前层的forward指针跳到当前这个节点中间会跳过多少的节点,Redis在插入删除操作时会更新span的大小,当我们要计算一个元素的排名时,只需要将span进行相加就能算出元素的最终的rank值

struct zslforward {

  zslnode* item;

  long span;  // 跨度

}

struct zsl {

  String value;

  double score;

  zslforward*[] forwards;  // 多层连接指针

  zslnode* backward;  // 回溯指针

}

猜你喜欢

转载自blog.csdn.net/zanpengfei/article/details/86518799