Redis5.0.9 一篇理解数据结构之跳跃表[zskipList]

目录

1.有序链表

2.跳跃表

3.跳跃表的应用

4.源码分析

1.跳跃表节点 zskiplistNode -源码位置 redis/src/server.h

2.跳跃表结构 zskiplist  -源码位置 redis/src/server.h

3.zset 有序集合的新增 -源码位置redis/src/t_zet.c

4..zset 有序集合的删除 -源码位置redis/src/t_zet.c

5.删除跳跃表 -源码位置redis/src/t_zet.c


说到跳跃表需要先对有序链表做一定了解

1.有序链表

我们知道在有序链表中元素都是有序排列的,平均时间复杂度是O(N), 并且每个节点都有指向下一节点的指针,最后一个节点指向NULL ,查询时间复杂度是O(n)。 插入和删除的操作也都是需要找到合适位置再做next 指针的修改操作。

对于图中的有序链表,如果我们要查询到值为27的节点,是需要比较4次才可以。 2 --> 14--> 19--> 27

那如果对于有序链表我们做了分层,每一层都是一个有序链表,并且在查找时候从最高层级开始,比较节点后如果大于查找值或是Next 指向NULL ,则降一层级查找。这样查询的方式效率会有部分提升的。

2.跳跃表

在跳跃表中查询值为27 的节点。

1.第一步: 比较第二层 第0个节点是2,小于 27 ,向后比较第二层第1 个节点是19,小于27 。继续比较发现19 next 指向NULL

2.第二步:跳跃到第1层值为19的节点next 指向 52,大于27 ;

3.第三步:跳跃到第0层,节点27就是要查询的节点,结束。

总结是比较了3次 。 2 --> 19 --> 52  ; 

如果节点数量比较多时,跳过节点越多查询效率会大大提升。

以上就是跳跃表的设计思想。

3.跳跃表的应用

在Redis 中,跳跃表主要用于有序集合zset的底层实现 ;另一种有序集合的底层实现是压缩列表。在Redis 源码中我们可以看到两中数据结构的转换使用过程。

源码文件位置redis/src/t_zset.c

/**
* 当满足 server.zset_max_ziplist_entries == 0 或者 server.zset_max_ziplist_value
* 小于要插入元素的字符串长度 时,则使用跳跃表结构,否则使用压缩列表结构
*/
 if (server.zset_max_ziplist_entries == 0 ||
            server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
 {
          zobj = createZsetObject();  // 创建跳跃表结构
 } else {
          zobj = createZsetZiplistObject();  //创建压缩列表结构
 }
/**
*  当zset 中元素个数大于  server.zset_max_ziplist_entries 或者 插入元素的字符串长度大于server.zset_max_ziplist_value 时,会将zset 的底层实现由压缩列表转为跳跃表。 
*  目前在zset 转为跳跃表之后,即时元素被删除,也不会重新转为压缩列表
*/

   if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries ||
                sdslen(ele) > server.zset_max_ziplist_value)
                zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);

4.源码分析

1.跳跃表节点 zskiplistNode -源码位置 redis/src/server.h

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;   // 存储字符串类型数据
    double score;   // 存储排序的分值
    struct zskiplistNode *backward;   //后退指针,指向当前节点最底层的前一个节点,头节点指向NULL,从后向前遍历跳跃表使用
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 指向当前节点的下一个节点,最后一个节点指向NULL 
        unsigned long span;  //forward 指向的节点与当前节点之间的元素个数。  值越大,跳过的节点个数越多。
    } level[]; // 柔性数组。每个节点的数组长度不同,生成跳跃表节点时,随机生成1~64 的值,值越大出现的概率越低
} zskiplistNode;

2.跳跃表结构 zskiplist  -源码位置 redis/src/server.h

//通过跳跃表结构我们可以在O(1)的时间复杂度快速知道跳跃表的头节点、尾节点、高度、长度
typedef struct zskiplist {
    struct zskiplistNode *header, /指向跳跃表头节点,这个节点的level 数组元素个数是64 ,64个元素的forward 都指向NULL,ele 为NULL ,score 为0 ,不计入总长度
     *tail;  //指向跳跃表尾节点
    unsigned long length;   // 跳跃表的长度,是统计除头节点以外的节点数
    int level; //跳跃表的高度
} zskiplist;

3.zset 有序集合的新增 -源码位置redis/src/t_zet.c

/* 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));
    // update[]  是因为在插入节点时,需要更新节点每层的前一个节点,需要将每层需要更新的节点记录在此数组中
    // rank[]  记录当前层从header 节点到update[i] 节点所经历的步长  += level[i].span
    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;
    }
//随机获取插入节点的高度
    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;
    }
//插入节点,按照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++;
    }
  // 重新调整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;
}

4..zset 有序集合的删除 -源码位置redis/src/t_zet.c

/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    for (i = 0; i < zsl->level; i++) {
        if (update[i]->level[i].forward == x) {
            update[i]->level[i].span += x->level[i].span - 1;
            update[i]->level[i].forward = x->level[i].forward;
        } else {
            update[i]->level[i].span -= 1;
        }
    }
//调整backward
    if (x->level[0].forward) {
        x->level[0].forward->backward = x->backward;
    } else {
        zsl->tail = x->backward;
    }
// 调整跳跃表高度、长度
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    zsl->length--;
}

5.删除跳跃表 -源码位置redis/src/t_zet.c

/* Free a whole skiplist. */
void zslFree(zskiplist *zsl) {
    zskiplistNode *node = zsl->header->level[0].forward, *next;
    zfree(zsl->header);
//依次释放节点内存
    while(node) {
        next = node->level[0].forward;
        zslFreeNode(node);
        node = next;
    }
    zfree(zsl);
}

 

参考书籍: 《Redis 5设计与源码分析》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章