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設計與源碼分析》

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