Redis原始碼解析之跳躍表(一)

北洛發表於2021-06-18

跳躍表(skiplist)

有序集合(sorted set)是Redis中較為重要的一種資料結構,從名字上來看,我們可以知道它相比一般的集合多了一個有序。Redis的有序集合會要求我們給定一個分值(score)和元素(element),有序集合將根據我們給定的分值對元素進行排序。Redis共有兩種編碼來實現有序集合,一種是壓縮列表(ziplist),另一種是跳躍表(skiplist),也是本章的主角。下面,讓筆者帶領大家稍微瞭解下有序集合的使用。

假設某軟體公司統計了公司內的程式設計師所掌握的程式語言,掌握Java的人數有90人、掌握C的人數有20人、掌握Python的人數有57人、掌握Go的人數有82人、掌握PHP的人數有61人、掌握Scala的人數有28人、掌握C++的人數有33人。我們用key為worker-language的有序集合來儲存這一結果。

127.0.0.1:6379> ZADD worker-language 90 Java
(integer) 1
127.0.0.1:6379> ZADD worker-language 20 C
(integer) 1
127.0.0.1:6379> ZADD worker-language 57 Python
(integer) 1
127.0.0.1:6379> ZADD worker-language 82 Go
(integer) 1
127.0.0.1:6379> ZADD worker-language 61 PHP
(integer) 1
127.0.0.1:6379> ZADD worker-language 28 Scala
(integer) 1
127.0.0.1:6379> ZADD worker-language 33 C++
(integer) 1

  

將上面的統計結果形成一個有序集合後,我們可以對有序集合進行一些業務上的操作,比如用:ZCARD key返回集合的長度:

127.0.0.1:6379> ZCARD worker-language
(integer) 7

  

可以把集合當成一個陣列,使用ZRANGE key start stop [WITHSCORES]命令指定索引區間返回區間內的成員,比如我們指定start為0,stop為-1,則會返回從索引0到集合末尾所有的元素,即[0,6],如果有序集合裡面有10個元素,則[0,-1]也代表[0,9],帶上WITHSCORES選項,除了返回元素本身,也會返回元素的分值。對比我們之前向有序集合插入元素的順序,以及下面返回元素的順序可以發現,有序集合會對我們所插入的元素進行排序。

127.0.0.1:6379> ZRANGE worker-language 0 -1 WITHSCORES
 1) "C"
 2) "20"
 3) "Scala"
 4) "28"
 5) "C++"
 6) "33"
 7) "Python"
 8) "57"
 9) "PHP"
10) "61"
11) "Go"
12) "82"
13) "Java"
14) "90"

 

獲取索引[2,5]的資料:

127.0.0.1:6379> ZRANGE worker-language 2 5 WITHSCORES
1) "C++"
2) "33"
3) "Python"
4) "57"
5) "PHP"
6) "61"
7) "Go"
8) "82"

  

除了可以根據索引來查詢元素,還可以通過ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]查詢分值區間的元素,並決定返回元素的偏移和數量,min為起始分值,max為結束分值,LIMIT選項會要求傳入兩個引數,offset和count,分別是:偏移量和根據偏移量返回的最大條目。

127.0.0.1:6379> ZRANGEBYSCORE worker-language 25 85 WITHSCORES
 1) "Scala"
 2) "28"
 3) "C++"
 4) "33"
 5) "Python"
 6) "57"
 7) "PHP"
 8) "61"
 9) "Go"
10) "82"
#[25,85]區間的元素有[Scala,C++,Python,PHP,Go],加上LIMIT選項僅查詢從偏移1開始(包含偏移1)往後的3個元素
127.0.0.1:6379> ZRANGEBYSCORE worker-language 25 85 WITHSCORES LIMIT 1 3
1) "C++"
2) "33"
3) "Python"
4) "57"
5) "PHP"
6) "61"

   

至此我們大致瞭解有序集合的使用,這裡就不再對有序集合的API做過多介紹,有興趣可以查閱官方文件。

下面我們說說有序集合的使用場景,得益於Redis的有序集合,使得很多高併發場景下的業務得以實現,例如:排行榜、聊天服務。像新浪微博的熱搜,本質上就是一個排行榜,根據使用者對話題的點選量或者討論程度對話題進行排序;聊天服務某種程度上也可以看做一個排行榜,因為比較早的聊天內容都排在前面。如果用有序集合來實現熱搜,我們可以用話題作為元素,使用者點選討論量作為分值;同理如果用有序集合來實現聊天服務,我們可以把使用者id和使用者聊天內容作為元素,而聊天時間戳作為分值。

從上面的業務場景,大家可以知道Redis的有序集合可以適配很多業務場景,那麼下面筆者就帶大家來了解下跳躍表的資料結構。

在Redis中如果一個有序集合的編碼是skiplist,實質上用的是zset這個資料結構,根據下面的節選可以看到,zset本身又包含兩種資料結構:dict和zskiplist。這兩個資料結構一目瞭然,dict是字典、zskiplist即是跳躍表,為何zset除了zskiplist還需要dict?這個問題筆者會在後面解釋,我們先來了解zskiplist和zskiplistNode 的資料結構。

server.h

typedef char *sds;

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

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

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

    

觀察zskiplist和zskiplistNode ,我們可以從zskiplist的欄位裡面發現有一個頭指標(header)和一個尾指標(tail),而zskiplistNode的欄位裡有一個backward指標,level陣列裡每個元素都有一個forward指標,我們可以猜測,跳躍表具有雙端連結串列的特性。除了backward指標和level陣列,zskiplistNode還有ele和score兩個欄位,ele是char型別的指標,score是double型別。因此,我們基本可以確定之前加入到跳躍表的元素和分值是儲存在zskiplistNode這個資料結構中的。每個跳躍表節點(zskiplistNode )會有一個後繼指標,指向自己身後的元素,並且level陣列中每一層都可能儲存一個前驅指標forward和一個跨度span,forward指標會指向往前一個、往前兩個……往前N個的節點,當我們要查詢一個節點,可以從高層橫跨多個節點往前遍歷,跳躍表由此得名。

zskiplist還剩length和level兩個欄位,length代表這個跳躍表有多少個跳躍表節點,level代表跳躍表的最高層高。可能還有人對上面對跳躍表的描述存有疑惑,這裡筆者畫了一幅跳躍表的案例圖,我們可以根據下圖加固一下對跳躍表的理解。

 

如上圖所示,跳躍表(zsl)會指向一個表頭(header),這個表頭的層高一般為32,表頭不會存放元素(ele)和分值(score),也沒有後繼指標(backward),所以表頭這三塊記憶體置灰。表頭有32層可用跳躍層(zskiplistLevel),這32層不一定會全部都用,具體看目前儲存元素和分值的節點中最高層是多少。比如上圖最高層為Python節點的6層,所以表頭的已使用層高也為6。儲存元素和分值的節點會有一個隨機演算法去生成層高,越往上的層高越難達到,從上圖我們也可以看到,除了表頭,每個節點的跳躍層(zskiplistLevel)高度都是不固定的。跳躍層有兩個欄位:forward和span,forward用於儲存前驅節點的地址,span(跨度)有兩個作用:(1)如果forward不為NULL,則用於記錄當前節點到達前驅節點本身在內需要經歷幾個節點,比如:表頭的L3層到達Python節點,需要經歷4個節點;C節點的L2層到達C++節點,需要經歷2個節點;Python節點的L1層到達Java節點,需要經歷3個節點。(2)如果forward為NULL,則記錄當前節點之後有多少個節點,比如上圖的Python節點,從L3~L5的forward都為NULL,則span記錄從Python節點之後還存在多少個節點,如果是跳躍表末尾的節點,那麼forward為NULL,span為0。

得益於層高的不固定,跳躍表相比一般的連結串列多了一個跳躍的功能,比如我們想查[80,100]的元素,我們不需要像連結串列一樣逐個遞進判斷分值是否落在我們的區間內,我們只要找到跳躍表中分值小於80但最靠近80的節點,這裡我們可以從頭結點的最高層(L5)開始遍歷,直接到達Python結點,Python結點的分值為57,小於80,於是我們基於Python結點逐層判斷該層是否包含前驅節點,Python.level[5].forward、Python.level[4].forward、Python.level[3].forward都為NULL,直到Python.level[2].forward(PHP)不為空,這裡判斷PHP.score依舊小於80,於是我們前進到PHP節點並基於該節點逐層往下判斷,PHP.level[2].forward.score(Java.score)為90>80,這裡我們不能再往前進,依舊基於PHP節點逐層下降,PHP.level[1].forward.score(Go.score)為82>80,依舊不能前進,要基於PHP節點下沉,一直到PHP.level[0].forward.score(Go.score)為82>80,我們就可以確定我們要基於PHP節點是最小於80但最靠近80的節點。於是,我們便基於PHP節點從L0開始逐個遞進,查詢分值介於[80,100]區間的節點,這裡從L0層逐個遞進是因為L0層相當於普通連結串列,不會出現跳躍的情況。

除了可以用分值來查詢元素,還可以將跳躍表當成一個陣列,根據索引來查詢元素,比如我們想查詢索引5之後的所有元素,即[5,-1],我們可以獲取這個跳躍表的長度推算末尾索引為7+(-1)=6,所以我們只要查詢索引落在[5,6]區間的元素就行,換算成跨度(span),我們要尋找跨度落在[6,7]之間的元素。這裡我們依舊是從頭節點的最高層開始遍歷,從頭節點的L5層出發,我們前進4個節點到達Python節點本身,此時跨度為4,Python.level[3,5]沒有前驅節點,於是基於Python節點下降到L2,Python.level[2]前進一個跨度到PHP節點,此時跨度為5,處於PHP節點的L2層的跨度為2,5+2>6,所以我們需要基於PHP節點下沉一個跳躍層到L1,PHP.level[1].span為1,表明只要我們前進到PHP.level[1].forward所指向的節點,就是我們的開始節點。只要找到開始節點,我們從開始節點的L0層逐個遞進,最終就能找到跨度落在[6,7],即索引在[5,6]的元素。

至此,我們大約瞭解了跳躍表的結構以及它是如何根據分值和索引查詢元素。那麼接下來,我們要真正進入到跳躍表的原始碼來了解它的資料結構。

如下程式碼所示,在建立跳躍表時,會為跳躍表分配一塊記憶體區域,並設定初始層高為1,元素個數為0,之後建立頭結點,頭結點的層高為32,頭結點沒有元素、分值和後繼指標,建立好頭結點後,初始化頭結點每個跳躍層的前驅指標為NULL、跨度為0,再設定跳躍表的初始尾結點為NULL,如此,一個跳躍表就初始化完畢。

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

/* t_zset.c
 * Create a new skiplist. 
 建立並返回一個跳躍表
*/
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
  
    //分配空間 
    zsl = zmalloc(sizeof(*zsl));
  
    //設定起始層次 
    zsl->level = 1;
    //初始元素個數為0
    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;
    }
    
    //設定表頭後繼指標為NULL
    zsl->header->backward = NULL;
    
    //初始表尾指標為NULL
    zsl->tail = NULL;
    return zsl;
}

//建立跳躍表元素 
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    /**
     * zskiplistNode最後一個欄位為彈性陣列,在計算記憶體佔用時不佔空間,需要
     * 額外分配level*sizeof(struct zskiplistLevel)個位元組空間用於存放層高
     */
    zskiplistNode *zn =
        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->ele = ele;
    return zn;
}

  

我們模擬下如何向一個跳躍表插入一個節點,來看下面的跳躍表,假設下面的跳躍表是儲存某一公司員工們的體重,我們先不管Amy、Lucy、John、Jack、Sam是如何插入到跳躍表中的,我們便基於現有的跳躍表結構,向跳躍表插入一個元素為Rose,分值為95.3的元素。

        

遍觀整個跳躍表,我們可以一目瞭然的確定,Rose應該插在Lucy和John兩個節點之間(93.7<95.3<120.5),但計算機沒有我們的上帝視覺,所以我們還是要以計算機的角度來查詢最適合插入John的位置。John既然要成為跳躍表中的節點,有一點是必不可少的,就是John一定是某些節點的前驅節點,也是某些節點的後繼節點。

首先,我們要先查詢有哪些節點有可能成為Rose的後繼節點,以及記錄後繼節點在跳躍表中的索引(注:這裡的索引也包含頭節點),方便在插入新節點時,同時更新跨度。查詢後繼節點索引從最高層開始,初始索引值為0,如果我們所處的層高允許我們從當前節點進入到下一個節點,則把當前節點的跨度加到索引值上。下一層的後繼節點索引值基於上一層統計的索引值,再判斷當前層所處的節點是否允許前進到下一個節點,允許的話就把當前層的跨度加到索引值上。

我們從頭節點(header)的最高層出發,頭節點初始索引為0,header.level[3].forward.score(Jack.score)為131.8,由於Jack.score>Rose.score(131.8>95.3),所以我們不能基於Jack節點前進,並且記錄header為L3層的後繼備選節點,記錄L3層的後繼節點索引為0;同理header也為L2層的後繼備選節點,L2層的後繼節點索引也為0。到達L1層時我們判斷header.level[1].forward.score(Amy.score,96.2)<120.3,於是我們從頭節點前進到Amy節點,當嘗試前進到Amy.level[1].forward節點時,判斷Amy.level[1].forward.score(Jack.score)>120.3不能前進,所以我們記錄Amy節點為L1層的後繼備選節點,L1層的後繼節點索引為1。之後,我們要計算L0的後繼節點及索引,這裡我們不再從header節點的L0層逐個遞進,而是基於Amy節點下沉至L0逐個遞進,在向Amy.level[0]層遞進前,我們先把L1層統計的後繼節點索引值作為L0層的初始索引值,因為我們在L0層是基於Amy節點開始遞進,而不是基於header節點遞進,要彌補在L0層從header遞進到Amy節點損失的索引值。我們基於Amy.level[0]一路前進到Lucy節點,將Lucy節點作為L0層的後繼備選節點,而Lucy不能再前進到John,所以L0層的初始索引值+Lucy.level[0].span=1+1=2。至此,我們完成了最高層L3到L0的後繼節點及其索引的統計,我們用兩個陣列update和rank分別記錄Rose的後繼備選節點update[Lucy,Amy,header,header]以及不同層後繼節點的索引rank[2,1,0,0]。

記錄Rose的後繼備選節點update很好理解,我們可以在不同層將原先後繼節點指向的前驅節點賦值給Rose的前驅節點,再將後繼節點的前驅節點指向Rose,這樣就相當於把Rose節點插入到跳躍表。那麼我們又如何根據不同層的後繼節點索引來更新跨度呢?我們用rank[0]-rank[i](i<zsl->level)得到的結果為新插入節點到每一層後繼節點中間的節點個數,比如rank[0]-rank[1]=1,Rose節點和Amy節點中間隔了一個Lucy節點,rank[0]-rank[2]=2,Rose節點和header節點中間隔了Amy和Lucy節點。所以,新插入節點到前驅節點的跨度為原先後繼節點的跨度-(rank[0]-rank[i]),後繼節點到新插入節點的跨度為rank[0]-rank[i]+1。

現在,讓我們為Rose生成一個隨機層高,並根據層高、分值和元素建立一個新的節點。這裡我們分3種情況來討論John節點的插入:

  1. Rose的層高小於當前跳躍表最高層高。
  2. Rose的層高等於當前跳躍表最高層高。
  3. Rose的層高高於當前跳躍表最高更高。

首先,我們來討論Rose的層高小於最高層的情況,假設隨機演算法為Rose生成了L1的層高,我們從L0層開始更新跳躍表的引用,從上面的分析結果我們可以知道,Rose的L0後繼節點為Lucy,我們將Lucy的前驅節點賦值給Rose的前驅節點:Rose.level[0].forward=Lucy.level[0].forward(John),接著我們更新Lucy的前驅節點為Rose:Lucy.level[0].forward=Rose,之後我們要更新L0層Lucy和Rose節點的跨度,由於是在L0層,相當於普通連結串列,所以Lucy在L0層指向Rose的跨度和Rose在L0層指向John的跨度都為1。

然後,我們在更新L1層的引用和跨度,Rose在L1層的後繼節點為Amy,這裡我們依舊把Amy在L1指向的前驅賦值給Rose在L1的前驅:Rose.level[1].forward=Amy.level[1].forward(Jack),再把Amy在L1的前驅指向Rose:Amy.level[1].forward=Rose。接著我們更新Amy和Rose的跨度,這裡先更新Rose指向Jack的跨度,再更新Rose:Amy指向Rose的跨度,因為在更新Rose指向Jack的跨度需要用到原先Amy在L1層指向Jack的跨度:Rose.level[1].span=Amy.level[1].span-(rank[0]-rank[1])=2,接著更新Amy指向Rose的跨度:Amy.level[1].span=(rank[0]-rank[1])+1=2。

L2和L3層的後繼節點都是header,由於Rose節點沒有足夠的層高讓其指向,所以這裡我們簡單地對L2和L3的後繼節點的跨度+1即可。

  

再來,我們考慮第二種情況,如果給Rose生成的層高剛好等於跳躍表當前的層高的,即Roese的層高為L3。這裡我們依舊延續之前的思路,先更新新插入節點的前驅節點跨度,再更新後繼節點指向新節點的跨度。Rose節點L0、L1如何更新看上一個步驟,這裡不再贅述。我們從L2開始更新,這裡筆者重新貼以上Rose節點未插入前的後繼備選節點update:[Lucy,Amy,header,header]及不同層後繼節點的索引rank:[2,1,0,0]。首先將L2的後繼節點指向的前驅賦值給Rose節點L2的前驅:Rose.level[2].forward=header.level[2].forward(Jack),再將L2後繼節點的前驅指向Rose節點:header.level[2].forward=Rose。接著我們更新跨度,Rose.level[2].span=header.level[2].span-(rank[0]-rank[2])=4-2=2,header.level[2].span=(rank[0]-rank[2])+1=3。L3同L2也是一樣的邏輯,這裡就不再演示。

  

最後,我們考慮Rose節點大於跳躍表最高層。首先我們思考下,假設隨機演算法為Rose生成的層高為L4、L5……L6甚至L31,目前跳躍表最高層高為L3,那麼大於L3之後的層高的後繼節點是什麼呢?也就只有頭節點了,那麼大於跳躍表更高層的後繼節點索引也就呼之欲出了,頭節點的索引只能是0。因此,我們的update和rank陣列需要更新為[Lucy,Amy,header,header,header……header]和[2,1,0,0,0……,0],那麼新層高的跨度又要怎麼計算呢?首先頭節點到新節點的跨度依舊是(rank[0]-rank[n])+1,其次新節點>L3的層高的前驅節點為NULL,層高中的跨度要更新為當前節點之後節點的個數,需要用跳躍表中元素的個數減去新插入節點到後繼節點(頭節點)之間節點的個數,即zsl-length-(rank[0]-rank[n])=5-2=3(n>3)。所以只要Rose節點的層高大於L3,L3之上的forward為NULL,跨度為Rose之後節點的個數(John、Jack、Sam),而header.level[n]到Rose的跨度為(rank[0]-rank[n])+1=2+1=3(n>3)。

 

按照之前筆者模擬的新節點插入到跳躍表的思路,我們來看下Redis又是如何實現的:

/* 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) {
    /*
     * update陣列會記錄每一層最靠近新插入節點的後繼節點,rank陣列
     * 最靠近新插入節點的後繼節點索引。
     */
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));

    x = zsl->header;//<1>
    //從頭節點最高層出發,查詢每一層最靠近新節點的後繼節點
    for (i = zsl->level - 1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position
         * rank陣列記錄每一層最靠近新節點的後繼節點索引,從最高層開始查詢時初始索引為0,
         * 即初始rank[zsl->level-1]=0,如果能從當前節點前進到下一個節點,則把當前節點的跨度
         * 加到索引值上。當處於i層前進到最靠近新節點的後繼節點不能再前進時,則下沉一層,
         * 基於當前前進到的後繼節點繼續嘗試往i-1層前進,而i-1層的初始索引值為i層最後統計的
         * 索引值,因為i-1層不會從header節點逐個遞進,而是基於上一層前進到的節點遞進,所以
         * 要彌補i-1層的索引要彌補header前進到當前節點的索引值。
        */
        rank[i] = i == (zsl->level - 1) ? 0 : rank[i + 1];
        /*
         * x初始值為header(見<1>處),如果x在i層的前驅節點不為NULL,則x有可能前進到前驅節點,
         * 即x在i層指向的前驅節點有可能成為新節點的後繼節點,但x能否前進要進一步判斷,這裡有
         * 兩個條件決定前進:
         * 1.如果x前驅節點的分值小於新節點的分值,則可以前進(x->level[i].forward->score < score)
         * 2.前驅節點的分值與新節點分值相同,且前驅節點的字串小於新節點的字串,則允許前進。注意:
         * 這裡比較字串並非比較字串長度,sdscmp(x->level[i].forward->ele,ele)會進一步呼叫
         * 函式memcmp(s1,s2,n),比較兩個字串的s1和s2的前n個位元組。
         * (x->level[i].forward->score == score &&
                    sdscmp(x->level[i].forward->ele,ele) < 0)
         */
        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)))  //
        {
            /*
             * 如果判定x節點可以前進到前一個節點,則把當前節點的跨度加到索引值上,
             * 並將x指向的記憶體區域更新為下一個節點。
             */
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        //x一直前進到不能再前進,即代表x所指向的節點為i層最靠近新節點的後繼節點,記錄後繼節點
        update[i] = x;
    }
    /* we assume the element is not already inside, since we allow duplicated
     * scores, reinserting the same element should never happen since the
     * caller of zslInsert() should test in the hash table if the element is
     * already inside or not. 
     * 
     * */
    /*
     * 這裡會用隨機演算法生成一個層高,越往上的層高越難生成,
     * 並且保證層高一定<=ZSKIPLIST_MAXLEVEL,下面會附上
     * 生成層高的程式碼。
     */
    level = zslRandomLevel();
    /*
     * 如果當前生成的層高大於跳躍表最高層高,還需要更新大於等於zsl->level
     * 層的後繼節點和節點索引。大於等於zsl->level層的後繼節點只能是頭節點,
     * 因此後繼節點索引都為0,同時更新這些後繼節點的跨度為跳躍表節點數,便於
     * 後續更新跨度。
     */
    if (level > zsl->level) {//<2>
        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);

    //將新節點插入到跳躍表中,並更新跨度
    for (i = 0; i < level; i++) {
        /*
         * update[i]為新節點在i層的後繼節點,將新節點在i層指向的前驅更新
         * 為後繼節點指向的前驅。再將後繼節點在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
         * (rank[0] - rank[i])為新節點和第i層後繼節點中間隔了多少個節點,
         * 在i層x到前驅節點的跨度(x->level[i].span)為原先後繼節點的跨度減去
         * 新節點和第i層後繼節點中間的節點個數:
         * (update[i]->level[i].span - (rank[0] - rank[i]))
         * 後繼節點在新節點的跨度為:(rank[0] - rank[i]) + 1
         * 如果程式之前曾進入到分支<2>,即執行level = zslRandomLevel()生成的層高
         * 高大於原先跳躍表的層高,比如原先zsl->level為3,但level生成為10,則新節點L3~L9
         * 的後繼節點為頭節點,後繼節點索引值為0,新節點L3~L9的前驅為NULL,跨度為新節點之後
         * 的節點個數,而zsl->length - (rank[0] - rank[i])為新節點之後的節點個數。
        */
        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 
     * 如果level = zslRandomLevel()生成的層高大於原先zsl->level的
     * 層高,則不會進入此迴圈。只有當生成的層高小於原先zsl->level的層高,
     * 才需要對level<=i<zsl->level的後繼節點中的跨度+1,因為新插入一個節點。
     */
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }

    /*
     * L0層相當於普通連結串列,如果新插入節點在L0層的後繼節點為header,則新節點的backward指向NULL,
     * 否則指向跳躍表中最靠近新節點的後繼節點。
     */
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    /*
     * 如果新節點不是末尾節點,則將新節點的前驅指點的backward指標指向新節點,
     * 否則將跳躍表的末尾指標指向新節點。
     */
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    //插入新節點後,對跳躍表的元素個數+1
    zsl->length++;
    return x;
}

  

生成層高zslRandomLevel(void):

/* Returns a random level for the new skiplist node we are going to create.
 * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
 * (both inclusive), with a powerlaw-alike distribution where higher
 * levels are less likely to be returned. 
 *   
 *              Skiplist P = 1/4  
 *              while 迴圈中   
 *              迴圈為真的概率為 P(1/4)
 *              迴圈為假的概率為 (1 - P)
 *               
 *              層高為1 的概率為           (1 -P)
 *              層高為2 的概率為        P *(1 -P)
 *              層高為3 的概率為      P^2 *(1 -P)
 *              層高為4 的概率為      P^3 *(1 -P)
 *              層高為n 的概率為  P^(n-1) *(1 -P)
 * 
 *             因此平均層高為 
 *             E = 1*( 1-P) + 2*P( 1-P) + 3*(P^2) ( 1-P) + ... 
 *               = (1-P) ∑ +∞ iP^(i-1)
 *                         i=1
 *               
 *               =1/(1-P) 
 * 
 * */
int zslRandomLevel(void) {
    int level = 1;
    while ((random() & 0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

    

相關文章