Redis底層詳解(六) 跳躍表

一、跳躍表概述

       跳躍表是有序集合的底層實現之一。

       1、跳躍表結點
       跳躍表的結點 zskiplistNode 定義在 server.h 中,定義如下:

typedef struct zskiplistNode {
    robj *obj;                              /* a */
    double score;                           /* b */
    struct zskiplistNode *backward;         /* c */
    struct zskiplistLevel {                 /* d */
        struct zskiplistNode *forward;     
        unsigned int span;                  
    } level[];
} zskiplistNode;

        a、robj 是 redisObject 的別名,在跳躍表中它的類型是一個 sds 字符串 (見 Redis底層詳解(二) 字符串);
        b、score 是一個浮點類型的數值,obj 和 score 共同構成了跳躍表元素的排序依據。score 爲排序的第一關鍵字,obj 爲排序的第二關鍵字(score 不同,按照 score 從小到大排;score相同,按照 obj 字符串進行字節排序 memcmp);
        c、backward 是指向跳躍表當前結點的前一個結點的指針;
        d、每個跳躍表結點有一個 level 數組,數組最大長度爲 32,數組元素類型爲 zskiplistLevel 。它記錄了每個 level 下當前結點鏈接到的下一個結點的前進指針 forward ,以及跨度 span (下文會詳細介紹這個鏈接關係); 

        2、跳躍表
        跳躍表結點被跳躍表結構 zskiplist 管理,定義在 server.h中:

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

        a、header 指針指向跳躍表頭結點,一旦創建後固定不變,tail 指向尾結點(當表爲空時值爲 NULL);
        b、length 記錄整個跳躍表的長度,便於在 O(1) 的時間內獲取表長度;
        c、level 代表跳躍表的最高層數,初始化爲1;

        3、跳躍表詳解
        看到這裏,如果之前沒有接觸過跳躍表,應該已經一頭霧水了。那麼好,接下來圖文並茂的時候到了。下圖代表的是一個擁有三個元素的跳躍表,分別爲 (3, "three"), (7, "seven"), (9, "nine"):

       上圖代表了一個三個元素的跳躍表。其中綠色格子是 zskiplist 部分,藍色格子是 zskiplistNode 部分。圖片從下往上看是內存遞增的方向,即 綠色 header 代表跳躍表的首地址, 藍色 obj 代表跳躍表結點的首地址。
        當跳躍表中元素爲 n 個時,其實有 n+1 個結點,多出來的那個結點就是跳躍表的頭結點,頭結點的 score 值爲 0,obj 置 NULL,backward 後退指針指向 NULL,並且默認有 32 個層 level[0...31]。
        綠色部分:整型值 level 代表除了頭結點以外,其它結點的層高的最大值(這裏爲 4 );length 表示實際元素個數(這裏爲 3);tail 指向跳躍表的尾結點;header 指向跳躍表的頭結點(固定不變)。
        藍色部分:每個跳躍表結點都有一個後退指針 backward,用來指向鏈表結構中的前一個結點;而 level [] 數組的每個元素是一個由 前進指針 forward 和 跨度span 組成的 zskiplistLevel 結構。除了頭結點外,其它結點的層高是在這個結點創建的時候隨機出來的,(score,obj)則是用來對跳躍表進行排序的排序依據。
        紅色曲線:代表每個結點在當前層的 forward 指針,這個指針一定是指向一個結點的首地址,而非 zskiplistLevel 結構的地址。
        橙色數字:代表每個結點在當前層指向的結點到當前結點的跨度 span。這個跨度的計算很容易從圖中看出,如果把這個跳躍表橫向理解成一個數組,那麼跨度就代表紅色曲線兩頭的兩個結點的 Rank(接下來會介紹 Rank 的含義)之差。
        注意:爲了區分各種指針,我們把 header 和 tail 的指針用黑色曲線表示;backward 的指針用灰色表示;forward 的指針用紅色表示。所有的這些指針要麼是 NULL,要麼指向 跳躍表結點 的首地址。

二、跳躍表概念

        1、層後繼結點

        每個結點在創建的時候,會隨機一個 [1,32] 的數 lv,作爲結點的層高,並且創建 lv 個 zskiplistLevel 結構。每個結構會有一個 forward 指針 和 span 跨度,如下圖中的紅色曲線代表 forward 指針,橙色數字代表 span 跨度:

        圖中結點 A 的第 0、1 層的 forward 指針指向 B,跨度爲 1; 第 2 層的 forward 指針指向 C,跨度爲 2。
        這裏 B 就是 A 在第 0 和 第 1 層的 後繼結點,而 C 則是 A 在第 2 層的後繼結點。 同理,C 也是 B 在第 0 和 第 1 層的後繼結點(並且,C 也是頭結點在第 3 層的後繼結點)。

        2、層前驅結點

        和後繼相對應的就是前驅結點,結點 A 在第0、1層是結點 B 的前驅結點,在第 2 層則是結點 C 的前驅結點。

        3、Rank

        跳躍表中一個很重要的概念就是 Rank,它代表每個結點在跳躍表中的相對位置 (類似數組下標)。Rank 從 1開始計數,如圖所示,三個結點的 Rank 分別爲 1 、 2 、 3:

        我們可以通過 zslGetRank 接口來獲取 (score, obj) 這個結點在給定跳躍表 zsl 中的 Rank,如果結點不存在則返回 0; 

unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) {
    zskiplistNode *x;
    unsigned long rank = 0;
    int i;
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&                                       /* a */
            (x->level[i].forward->score < score ||                          /* b */
                (x->level[i].forward->score == score &&                     
                compareStringObjects(x->level[i].forward->obj,o) <= 0))) {
            rank += x->level[i].span;                                       /* c */
            x = x->level[i].forward;                                        
        } 
        if (x->obj && equalStringObjects(x->obj,o)) {                       /* d */
            return rank;
        }
    }
    return 0;
}

        a、從最高層開始枚舉,對每一層找到  (score, obj)  的前驅結點;
        b、前驅結點的 score 要麼小於 當前結點的 score,要麼 score 和當前結點相等且 obj 的字典序比 當前結點的obj 小;
        c、對跨度進行累加,所有層的前驅結點的跨度之和就是最後要求的 Rank;
        d、爲了避免找到的 x 是頭結點,需要判斷 x->obj 不爲 NULL;

三、跳躍表操作

        1、創建跳躍表
        跳躍表的創建調用 zslCreate 接口,默認層數 level 爲 1, 跳躍表長度 length 爲 0,tail 置NULL, zslCreateNode 爲創建一個跳躍表結點的接口,這裏用來創建頭結點,函數實現在 t_zset.c 中:

zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
    zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->obj = obj;
    return zn;
}

zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
    zsl = zmalloc(sizeof(*zsl));                               
    zsl->level = 1;                                         
    zsl->length = 0;
    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;
}

.

       上圖代表了一個剛創建完的跳躍表,即空表。
        初始創建一個頭結點,score 爲 0,obj 置 NULL,backward 後退指針指向 NULL,並且生成 32 個層 level[0...31],圖中每個層的向右紅色箭頭表示 forward 指針,橙色數字代表跨度 span。初始化每個層的 forward 指向 NULL,跨度爲0。

        2、插入跳躍表結點
        跳躍表的插入有點類似鏈表,首先要找到一個插入位置,生成一個結點,然後修改插入位置的指針進行插入操作。結點插入的 API 爲 zslInsert,整個插入過程分爲以下四部分:
        a、尋找插入位置;
        b、隨機插入結點層數;
        c、生成插入結點並插入;
        d、額外信息更新;
        具體實現在 t_zset.c 中:

zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;
    serverAssert(!isnan(score));

    /************************* a、尋找插入位置 *************************/
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        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 &&
              compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    /************************* a、尋找插入位置 *************************/

    /*********************** b、隨機插入結點層數 ***********************/
    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;
    }
    /*********************** b、隨機插入結點層數 ***********************/

    /***********************  c、生成結點並插入  ***********************/
    x = zslCreateNode(level,score,obj);
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward;
        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;
    }
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    /***********************  c、生成結點並插入  ***********************/

    /***********************   d、額外信息更新   ***********************/
    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++;
    /***********************   d、額外信息更新   ***********************/
    return x;
}

       a、尋找插入位置。這步操作類似上文提到的計算 Rank 的過程。由於跳躍表是個有序表,所以 (score, obj) 一定是嚴格遞增的,比如 score = 4 時一定是插入到 score 爲 3 和 score 爲 7 的結點之間;score = 8 時一定是插入到 score 爲 7 和 score 爲 9 的結點之間(當然,如果 score 相同,則需要比較另一個關鍵字 obj 的大小關係,這裏爲了簡化問題,不再詳述);如下圖所示的紫色箭頭指示了插入位置:

         源碼的實現是從跳躍表的最大那層開始,對每一層進行統計。將插入的位置信息存儲在了兩個輔助數組 update[] 和 rank[] 中。其中, update[ i ] 表示將要插入位置在第 i 層的前驅結點。換言之,假設要插入的結點爲 x,那麼在執行完插入操作之後,update[i]->level[i] 的 forward 成員指向的就應該是 x 了 (顯然,在插入之前還不是,因爲我們還沒有生成 x 這個結點)。而 rank[ i ] 表示 update[ i ] 這個結點的 Rank 值 (如果 update[i] 是頭結點,那麼 Rank 值爲0)。
        b、隨機插入結點層數。如果新插入結點隨機得到的層數比之前的最大層數還要大,則 需要更新最大層數 level 以及超出部分的 update [] 和 rank [] 的值。
        c、生成結點並插入。調用 zslCreateNode 生成結點 x,遍歷結點的每一層,將這個結點每一層的後繼結點 (x->level[i].forward)指向對應的 update[i] 在該層的後繼結點 (update[i]->level[i].forward)。然後將 update[i] 在該層的後繼結點修改爲 x。這一步操作和普通鏈表的插入操作一致。然後利用 rank[] 數組修改每一層的跨度。
        d、額外信息更新。主要是更新 backward 指針。 令插入結點爲 x,如果 x 在跳躍表的第一個元素,那麼它的 backward 指針置爲NULL,否則指向前一個結點(即 update[0]);如果 x 在跳躍表的最後一個元素,那麼跳躍表的 tail 指針指向 x,否則 x 的相鄰的下一個結點的 backward 指針置爲 x。最後,跳躍表 length 屬性自增1。
        依次插入三個元素的展示如下:

        3、刪除跳躍表結點

        刪除結點的過程是插入的逆過程,如果已經理解了插入,那麼刪除將完全不成問題。刪除的 API 爲 zslDelete,在 t_zset.c 中,實現如下:

int zslDelete(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;
    /************************* a、尋找待刪除結點 *************************/
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                compareStringObjects(x->level[i].forward->obj,obj) < 0)))
            x = x->level[i].forward;
        update[i] = x;
    }
    x = x->level[0].forward;
    /************************* a、尋找待刪除結點 *************************/

    /************************* b、執行結點的刪除 *************************/
    if (x && score == x->score && equalStringObjects(x->obj,obj)) {
        zslDeleteNode(zsl, x, update);
        zslFreeNode(x);
        return 1;
    }
    /************************* b、執行結點的刪除 *************************/

    return 0;
}

        a、尋找待刪除結點。這步操作類似上文提到的插入結點的過程。update[] 代表待刪除結點在每一層上的前驅結點,從最高層往下遍歷,最後得到的 x 就是待刪除節點。
        b、執行結點的刪除。如果待刪除結點的 score 和 obj 與 傳參不完全相等,說明這個結點不存在,返回 0; 否則, 調用 zslDeleteNode 執行結點的刪除。最後調用 zslFreeNode 進行內存釋放。
        zslDeleteNode 的實現如下:

void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    for (i = 0; i < zsl->level; i++) {
        if (update[i]->level[i].forward == x) {                        /* a */
            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;                            
        }
    }
    if (x->level[0].forward) {                                         /* b */
        x->level[0].forward->backward = x->backward;
    } else {
        zsl->tail = x->backward;
    }
    while(zsl->level > 1 &&                                           /* c */
          zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;
    zsl->length--;
}

        a、遍歷所有層 i,如果待刪除結點 x 的層高小於 i,顯然這一層的前驅結點 update[i] 在第 i 層的的後繼結點不會是x,所以只需要將前驅結點的第 i 層的跨度 span 減1 即可;否則,前驅結點的第 i 層的後繼結點就是 x,這時候需要根據 x 的 forward 和 span 對前驅結點的第 i 層 forward 和 span 進行更新;
        b、如果被刪除的結點是原跳躍表的最後一個結點(沒有後繼結點),則更新跳躍表的 tail 指針;否則,更新它後繼結點的 backward 指針;
        c、結點刪除後,如果刪除的結點的層高是其它所有結點中最高的 (沒有並列),那麼,勢必會導致整個跳躍表的最大層高的減少,這時就要將跳躍表的 level 字段進行更新。最後 length 自減 1。

        結點刪除後,需要將刪除的結點的內存釋放掉,否則就會引起內存泄漏。釋放內存的 API 是 zslFreeNode,實現如下:

void zslFreeNode(zskiplistNode *node) {
    decrRefCount(node->obj);
    zfree(node);
}

        decrRefCount 用來減少 obj 的引用計數,當計數爲 0 時,會自動將 obj 的內存釋放掉。如果沒有這一步, 調用 zfree 的時候只釋放 node 的內存,對於其中成員 obj 指向的那塊內存是不會進行管理的。

四、其他API

        跳躍表還有幾個和區間操作相關的 API,實現概述和複雜度如下,具體可以參看源碼:

        1、zslIsInRange
        給定一個 score 的 範圍 range,判斷跳躍表內是否有元素在這個 range 內。實現方式採用的是兩個區間進行判交,如果有交集返回1,否則返回0。算法的時間複雜度爲 O(1)。

        2、zslFirstInRange
        
獲取第一個 score 在 range 範圍內的跳躍表結點。實現方式和計算 Rank 的方式類似,核心就是找小於 range最小值 的且最接近它的結點,將的到的結點的直接後繼結點,如果它的 score 在 range 範圍內,返回這個結點;否則返回 NULL;期望複雜度是 O(log N) 的,但是最壞複雜度是 O(N) 的。

        3、zslLastInRange
        獲取最後一個 score 在 range 範圍內的跳躍表結點。實現方式參照 zslFirstRange 。

        4、zslDeleteRangeByScore
        刪除給定 score 範圍內的所有跳躍表結點。算法最壞複雜度 O(N)。

        5、zslDeleteRangeByRank
        刪除給定 Rank 範圍內的所有跳躍表結點。算法最壞複雜度 O(N)。

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