redis中跳錶的運用及原始碼解析(二)

熊景發表於2013-10-20

上篇中(click here), 我對跳錶的一些基本的概念及其在redis原始碼中的定義形式做了一些分析,同時也對跳錶為什麼能實現“跳躍”功能做了一番介紹。在這篇中,我將重點針對redis中跳錶的實現做詳細的分析;關於跳錶相關的定義以及程式碼位置,在上篇中已有說明。

我們接著上篇末尾列出的那幾個函式定義開始,這幾個函式包含了最基本的幾種操作,建立、插入、刪除、查詢;首先,我們看redis中是如何實現跳錶的建立的:

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

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;
}  

首先是跳錶結構的定義

struct zskiplistNode *header, *tail  

頭尾指標,表名是雙向連結串列。

unsigned long length;  

length是表示連結串列中節點的個數。

int level;  

表示跳錶的層數。
然後我們在看zslCreate函式的定義,其中最關鍵的就是裡邊的那個迴圈,

ZSKIPLIST_MAXLEVEL  

巨集定義,用於表示這個跳錶最多有多少層。在迴圈內,將指向後邊節點的forward指標置為NULL。 zslCreate呼叫完成後,其實就是建立了跳錶的頭結點(我們知道所有的連結串列實現中通常也是需要一個頭結點的)。

然後我們再來看如何給跳錶插入一個元素,插入元素程式碼請看t_zset.c,第107行zslInsert函式, 這裡不貼出完整的程式碼,我只取其中比較重要的部分分析。

插入一個元素到跳錶中,首先需要確定的是插入元素的位置,插入元素的時候需要保證跳錶元素的排列順序是升序排列;然後確定待插入節點的層數;最後是將節點各層指向正確的後續節點,然後插入元素。

首先請看下述程式碼段:

zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
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 &&
            compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
        rank[i] += x->level[i].span;
        x = x->level[i].forward;
    }
    update[i] = x;
}  

這段程式碼主要的作用就是查詢節點所要插入的位置。首先請看下邊這幅圖:
跳錶圖

通常,在跳錶中查詢元素的時候,不像在連結串列中查詢元素那樣需要遍歷,而是會從頭結點(頭結點的下一節點才是元素節點)的最頂層開始,即上述程式碼的for迴圈,如果level陣列的forward指標指向的節點的score值大於要插入的score,那麼就下降一層;否則,就把x前進一個節點,指向到下一個節點,繼續比較,即上述程式碼中while迴圈所做的工作。最後,當結束for迴圈時,update陣列中儲存的節點的forward就是將要插入的節點的level陣列中的forward需要的值,並且待插入的節點一定是位於update[0]這個指標所指節點的後邊

當新節點需要插入的位置找到後,就需要確定新節點的層數:

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;
}  

這裡需要指出的是為什麼新加的層節點直接用zsl->header賦值呢? 原因是這樣的,因為新加入的這層與zsl->header直接肯定是沒有其他節點的層的;而下邊我們在給初始化新插入節點的level陣列的時候,是把update的每一個元素當作其前一個跳躍節點的。

 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;

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

綜合這幾段程式碼,我們便可以知道update這個陣列的作用了;然後便是給backward賦值,以及修改zsl的tail指標:

 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++;  

到這裡,基本跳錶的建立以及插入元素都分析完畢了,下邊,我們看看如何刪除一個元素。刪除元素的程式碼位於t_zset.c 第185行zslDelete定義

同插入節點類似,刪除節點的時候,同樣也是需要先找到需要刪除節點的位置,程式碼如下:

zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i;

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;
}  

這裡,需要特別注意的是,到for迴圈結束時,update[0]處的指標所指向的是要刪除的節點的前一個節點,到這裡為止,要刪除的節點是否存在並不知道。

x = x->level[0].forward;  

這裡便是將節點往後前進一個位置,便到了我們要尋找的節點的位置,到這裡為止,要刪除的節點是否存在仍然不知道,下邊便是做節點的比較:

if (x && score == x->score && equalStringObjects(x->obj,obj)) {
    zslDeleteNode(zsl, x, update);
    zslFreeNode(x);
    return 1;
} else {
    return 0; /* not found */
}  

只有當score值與節點元素的值全部相等時,才說明要刪除的節點是存在的,否則就是不存在的。

最後,節點元素的查詢跟之前插入跟刪除節點的查詢過程是一樣的,具體請看redis的實現 t_zset.c unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) 這個函式。

相關文章