Redis数据结构和源码分析——跳表

131 阅读6分钟

结构体定义

结构体定义在server.h文件内

跳表节点数据结构

typedef struct zskiplistNode {
    sds ele;  /* 使用 SDS(Simple Dynamic Strings)存储的元素值 */
    double score;  /* 用于排序的元素分数 */
    struct zskiplistNode *backward;  /* 后向指针,指向前一个节点 */
    //节点的level数组,保存每层上的前向指针和跨度
    struct zskiplistLevel {
        struct zskiplistNode *forward;  /* 前向指针 */
        unsigned long span;  /* 到同一层级的下一个节点的跨度或距离 */
    } level[];
} zskiplistNode;

跳表结构体

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;  //跳表头尾节点
    unsigned long length; //跳表长度
    int level; //跳表最大层数
} zskiplist;

zset由dict和zskiplist组成

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

跳表节点查询过程

查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:

  • 如果当前节点的权重「小于」要查找的权重时,跳表就会访问该层上的下一个节点。
  • 如果当前节点的权重「等于」要查找的权重时,并且当前节点的 SDS 类型数据「小于」要查找的数据时,跳表就会访问该层上的下一个节点。当score相同时,会根据member排序,例如"apple"应该排在"banana"前面。

如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针,然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。

如果要查找「元素:abcd,权重:4」的节点,查找的过程是这样的:

  • 先从头节点的最高层开始,L2 指向了「元素:abc,权重:3」节点,这个节点的权重比要查找节点的小,所以要访问该层上的下一个节点;
  • 但是该层的下一个节点是空节点( leve[2]指向的是空节点),于是就会跳到「元素:abc,权重:3」节点的下一层去找,也就是 leve[1];
  • 「元素:abc,权重:3」节点的 leve[1] 的下一个指针指向了「元素:abcde,权重:4」的节点,然后将其和要查找的节点比较。虽然「元素:abcde,权重:4」的节点的权重和要查找的权重相同,但是当前节点的 SDS 类型数据「大于」要查找的数据,所以会继续跳到「元素:abc,权重:3」节点的下一层去找,也就是 leve[0];
  • 「元素:abc,权重:3」节点的 leve[0] 的下一个指针指向了「元素:abcd,权重:4」的节点,该节点正是要查找的节点,查询结束。
  • image.png

创建跳表

/* 创建一个新的跳表。*/
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    // 分配内存以存储跳表结构
    zsl = zmalloc(sizeof(*zsl));
    
    // 初始化跳表的基本属性
    zsl->level = 1;
    zsl->length = 0;
    
    // 创建头结点并初始化 ZXKIPLIST_MAXLEVEL定义的是最高层数,我的redis版本定义为32
    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;
}

#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^64 elements */

zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    // 分配内存以存储跳表节点结构及其层级信息
    zskiplistNode *zn = zmalloc(sizeof(*zn) + level * sizeof(struct zskiplistLevel));
    
    // 初始化节点的分数和元素值,score = 0、ele = null
    zn->score = score;
    zn->ele = ele;
    
    //返回该节点作为头节点
    return zn;
}

插入节点

zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele);
unsigned char *zzlInsert(unsigned char *zl, sds ele, double score);

这里有两个方法,第二个方法是向压缩列表中插入,我们看第一个跳表实现的。

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

    // 检查分数是否为 NaN(不是数字)
    serverAssert(!isnan(score));

    // 从头节点开始向下遍历每一层,寻找插入位置
    x = zsl->header;
    // level - 1,即从最高层开始向下
    for (i = zsl->level-1; i >= 0; i--) {
        /* 存储到达插入位置时经过的排名 */
        // 如果是最高层,rank[31] = 0
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        //forward指向同一层级下一个节点
        //下一个节点不为空 且 下一个节点的分数小于传入新元素分数 或 分数相同但新元素sds大于下一个节点sds
        //这种情况应该继续向后
        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;
        }
        //将i层找到的前驱节点x记录
        //目的是将新插入的节点 `x` 记录到 `update` 数组中,以便后续更新跳表的结构。
        update[i] = x;
        //i层查找结束,继续从x节点的下一层开始往后寻找
    }
    
    // 这是官方注释翻译,不产生相同元素由程序员保证
    /* 假设元素不存在,因为我们允许分数重复,重新插入相同元素应该不会发生,
     * 因为 zslInsert() 的调用者应该在哈希表中检查元素是否已存在。*/
     
    // 随机生成节点的层级
    // 内部实现:0 - 1范围内生成一个随机数,如果这个随机数小于 0.25(相当于概率 25%)
    //那么层数就增加 1 层,然后继续生成下一个随机数,
    //直到随机数的结果大于 0.25 结束,最终确定该节点的层数
    level = zslRandomLevel();

    // 如果生成的层级大于跳表的最大层级
    // 超出部分的层数将会被使用,将其记录到update供后续使用
    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);

    // 将新节点插入跳表中
    // 从第0层到第level层都需要插入
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward;
        //将 `update[i]->level[i].forward` 更新为新节点 `x`,以确保跳表中节点的正确连接关系。
        update[i]->level[i].forward = x;

        // 更新跨度
        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++;
    }

    // 设置新节点的 backward 指针
    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;
}