Redis源碼閱讀筆記-跳躍表結構 原

跳躍表

跳躍表 是一種有序的數據結構,通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。它是基於有序鏈表,在鏈表的基礎上,每個節點不止包含一個指針,還可能包含多個指向後繼節點的指針,從而加快操作。

示例:

跳躍表示例圖.png

結合圖,比如查找key爲3是否存在:

  1. 首先訪問的是key1,比3小,然後從最高層的後繼節點4進行比較,34小,所以3肯定在14之間;
  2. 因此移向下一層,後繼節點爲3剛好滿足。

時間複雜度

跳躍表支持平均O(logN),最壞O(N)時間複雜度的節點查找。

在大部分情況下,跳躍表的效率可以和平衡樹相媲美,並且跳躍表的實現比平衡樹簡單。

特點

來之《Redis設計與實現》

  • 跳躍表是有序集合的底層實現之一。Redis只在實現有序集合鍵和集羣節點的內部數據結構中用到了跳躍表。
  • Redis的跳躍表實現由zskiplist和zskiplistNode兩個結構組成,其中zskiplist用於保存跳躍表信息(比如表頭節點、表尾節點、長度),而zskiplistNode則用於表示跳躍表節點。
  • 每個跳躍表節點的層高都是1至32之間的隨機數(冪次定律,越大的數出現的 概率越小)。
  • 在同一個跳躍表中,多個節點可以包含相同的分值,但每個節點的成員對象必須是唯一的。
  • 跳躍表中的節點按照分值大小進行排序,當分值相同時,節點按照成員對象的大小進行排序。

代碼結構

// server.h

/* ZSETs use a specialized version of Skiplists */
// 跳躍表節點 結構
typedef struct zskiplistNode {
    // 節點的成員
    // 在redis3.0 中使用的是 robj
    // 在redis4.0 中使用 sds
    sds ele;
    double score; // 分值
    struct zskiplistNode *backward; // 後退指針
    
    // 層
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 層的前進指針
        unsigned int span; // 層的跨度
    } level[];
} zskiplistNode;

成員ele: 在同一個跳躍表中,各個節點保存的成員對象必須是唯一的。

分值score: 跳躍表中,所有節點都按分值從小到大排序。不同節點中的分值可以相同,分值相同的節點將按照成員對象的大小來排序,成員對象較小的節點在前面。

後退指針*backward: 用於從表尾向表頭方向訪問節點,跟可以一次跳過多個節點的前進指針不同,因爲每個節點只有一個後退指針,所以每次只能後退至前一個節點。

level[]:跳躍表節點的level數組可以包含多個元素,每個元素都包含一個指向其他節點的指針,程序可以通過這些層來加快訪問其他節點的速度,一般來說,層的數量越多,訪問其他節點的速度就越快。每次創建一個新跳躍表節點的時候,程序都根據冪次定律(powerlaw,越大的數出現的概率越小)隨機生成一個介於1和32之間的值作爲level數組的大小,這個大小就是層的“高度”。

前進指針*forward: 每個層都有一個指向表尾方向的前進指針,用於從表頭向表尾方向訪問節點。

跨度span: 層的跨度用於記錄連個節點之間的距離:

  • 兩個節點之間的跨度越大,它們相距得就越遠。
  • 指向NULL的所有前進指針的跨度都爲0,因爲它們沒有連向任何節點。
// 跳躍表 結構
typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 記錄 表頭節點 和 表尾節點 的指針
    unsigned long length; // 表中節點的數量
    int level; // 表中層數最大的節點層數
} zskiplist;

部分代碼解析

  • zskiplist *zslCreate(void) 創建一個空的zskiplist:

    	// t_zet.c
    
    	/* Create a new skiplist. */
    	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++) {
    	        // 爲頭節點的各個層級賦值
    	        // 前進指針forward 置爲 NULL
    	        // 跨度span 設爲 0
    	        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) 創建一個跳躍表節點,創建level層數,分值score,節點成員ele:

    	// t_zet.c
    
    	/* Create a skiplist node with the specified number of levels.
    	 * The SDS string 'ele' is referenced by the node after the call. */
    	zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    	    // 爲層數level數組分配內存
    	    zskiplistNode *zn =
    	        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    	    // 分值賦值
    	    zn->score = score;
    	    // 成員賦值
    	    zn->ele = ele;
    	    return zn;
    	}
    
  • zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) 往跳躍表*zsl 中插入成員ele,分值是score

    
    	// 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));
    	    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;
    	    }
    	    /* 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. */
    	    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;
    	    }
    	    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++;
    	    }
    
    	    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;
    	}
    
    • 插入第一個節點(假設節點score爲1, ele爲a):

      1. 初始化狀態

      2. 獲得插入位置

        x = zsl->header;
        // 當跳躍表本身爲空時,zsl->level 爲 1
        // 所以這個循環只執行1次
        for (i = zsl->level-1; i >= 0; i--) {
            /* store rank that is crossed to reach the insert position */
            // 所以rank[0] = 0
            rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        
            // 因爲 x 指向 zsl 的表頭節點,
            // 表頭節點 所有的level 的前進指針都爲NULL
            // 所以插入第一個節點上時,不進入這個循環
            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[0]指向表頭節點
            update[i] = x;
        }
        

      3. 插入節點

        	// 該函數會生成一個隨機(1-32之間)的level(假設爲4)
        	level = zslRandomLevel();
        	// 如果生成的level大於zsl->level(當前爲1)
        if (level > zsl->level) {
            // i 從 1 開始,循壞到 i = 3
            for (i = zsl->level; i < level; i++) {
                // rank[1],rank[2],rank[3] 設爲 0
                rank[i] = 0;
                // update[1],update[2],update[3] 都指向表頭節點
                update[i] = zsl->header;
                // update[1]->level[1].span,update[2]->level[2].span,update[3]->level[3].span 都設爲 0 
                // 實際上就是將 header的level[1,2,3]的span都設爲0
                update[i]->level[i].span = zsl->length;
            }
            // 更新zls->level的值
            zsl->level = level;
        }
        

        // 創建表節點
        	x = zslCreateNode(level,score,ele);
        	// level 爲4,循環 i = [0, 1, 2, 3]
        for (i = 0; i < level; i++) {
            // x 是指向新建節點的
            // update[0-3]是指向頭節點
            // 而頭節點的level[0-3]的前進節點都是NULL
            // 所以新建節點x->level[0-3]的前進節點都指向NULL
            x->level[i].forward = update[i]->level[i].forward;
        
            // 將頭節點的level[0-3]的前進節點都指向新建節點x
            update[i]->level[i].forward = x;
        
            /* update span covered by update[i] as x is inserted here */
            // 更新 新建節點x->level[0-3]的層跨度
            // 而 表頭節點->level[0-3].span 的層跨度都爲0
            // rank[0-3] 爲 0
            // 所以 新建節點x->level[0-3]的層跨度 都設爲0
            x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);	
            // 更新表頭節點的level[0-3]層跨度 = 1
            update[i]->level[i].span = (rank[0] - rank[i]) + 1;
        }
        

      4. 更新其他節點的level信息

        	/* increment span for untouched levels */
        	// level 爲4
        	// zsl->level 也爲 4
        	// 所以不進入該循環
        for (i = level; i < zsl->level; i++) {
            update[i]->level[i].span++;
        }
        
        // 新建節點的信息指向NULL
        x->backward = (update[0] == zsl->header) ? NULL : update[0];
        // x->level[0].forward 指向 NULL
        if (x->level[0].forward)
            x->level[0].forward->backward = x;
        else
            // 表中只有1個結點,將表尾指針指向新建節點x
            zsl->tail = x;
        // zsl 的長度 + 1
        zsl->length++;
        return x;
        

    • 向有數據的跳躍表插入

      1. 假設跳躍表中有如下節點,假設插入節點score爲2,ele爲b:

      2. 獲得插入位置

        	x = zsl->header;
        
        	// i 初始值爲 4,所以循環會執行 i[4, 3, 2, 1, 0] 5次
        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];
        
            // 循環尋找出要定位的位置
            // 直至找到末尾,或者參數分值小於節點分值
            // 如果找到相同分值,則比較成員的大小
            // 隨着 i 的變小,x將會指向插入位置前方的節點
            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的指向,在while循環中x向前移動
                x = x->level[i].forward;
            }
        
            update[i] = x;
        }
        

      3. 爲新增的節點隨機生成level(假設生成的是6)

        	// 獲取一個隨機的level數,假設生成的是6
        level = zslRandomLevel(); 
        if (level > zsl->level) {
        // 如果隨機生成的level 比原來跳躍表的level大,則需要對錶頭節點的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;
        }
        

      4. 創建並在跳躍表中插入新節點

        	   // 創建節點,level=6, sorce=2, ele="b"
        	   x = zslCreateNode(level,score,ele);
        	   for (i = 0; i < level; i++) {
        	     // 因爲 *update數組 中保存的都是在 插入位置 之前的節點
        	     // 所以在這裏將x中level的前進指針 指向 *update數組中節點的前進指針指向的節點
            x->level[i].forward = update[i]->level[i].forward;
            // 然後 *update數組中節點的前進指針 指向 x
            update[i]->level[i].forward = x;
        
            /* update span covered by update[i] as x is inserted here */
            // 更新 x 中各個level的跨度
            x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
            // 更新 *update數組中節點的level跨度
            update[i]->level[i].span = (rank[0] - rank[i]) + 1;
        }
        

      5. 更新其他節點的level信息

        	    /* increment span for untouched levels */
        	    // 更新其他節點的跨度,本例中level和zsl->level已經相等,所以不進入循環
        	    for (i = level; i < zsl->level; i++) {
        	        update[i]->level[i].span++;
        	    }
        		// 更新插入節點的後退指針
        	    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;
        

  • int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) 通過指定scoreelse刪除節點,如果在跳躍表中找到節點,函數將會返回1,並且將刪除的節點寫入**node中;如果沒有找到,將會返回0:

    	/* Delete an element with matching score/element from the skiplist.
    	 * The function returns 1 if the node was found and deleted, otherwise
    	 * 0 is returned.
    	 *
    	 * If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise
    	 * it is not freed (but just unlinked) and *node is set to the node pointer,
    	 * so that it is possible for the caller to reuse the node (including the
    	 * referenced SDS string at node->ele). */
    	int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
    	    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    	    int i;
    
    	    x = zsl->header;
    	    // 找出 要刪除節點 的前置節點,與zslInsert()方法中類似
    	    // 而update數組中保存了 要刪除節點 在刪除之前各個 level 緊鄰的前置節點
    	    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 &&
    	                     sdscmp(x->level[i].forward->ele,ele) < 0)))
    	        {
    	            x = x->level[i].forward;
    	        }
    	        update[i] = x;
    	    }
    	    /* We may have multiple elements with the same score, what we need
    	     * is to find the element with both the right score and object. */
    	    x = x->level[0].forward;
    	    // 判斷分值和成員是否相同
    	    if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
    	        zslDeleteNode(zsl, x, update);
    	        if (!node)
    	            // 如果參數node 是 NULL的,則直接釋放掉要刪除的節點
    	            zslFreeNode(x);
    	        else
    	            // 否則將node 指向 x
    	            *node = x;
    	        return 1;
    	    }
    	    return 0; /* not found */
    	}
    
    		/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */
    	// 刪除節點的內部函數
    	// *zsl 是指定的跳躍表
    	// *x 是要刪除的節點
    	// **update 是指向一個數組的指針,數組中保存了 要刪除節點 在刪除之前各個 level 緊鄰的前置節點
    	void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    	    int i;
    	    // 刪除節點x,並且更新表中x前置節點level的跨度和指向
    	    for (i = 0; i < zsl->level; i++) {
    	        if (update[i]->level[i].forward == x) {
    	            // 如果是本來指向x的,則指向x對應level的前置節點
    	            // 並且重新計算跨度
    	            update[i]->level[i].span += x->level[i].span - 1;
    	            update[i]->level[i].forward = x->level[i].forward;
    	        } else {
    	            // 如果本來不是指向x的,則是指向x之後的節點,所以只需要跨度減1
    	            update[i]->level[i].span -= 1;
    	        }
    	    }
    	    // 修改 要刪除節點x 的後一個節點的後退指針
    	    if (x->level[0].forward) {
    	        x->level[0].forward->backward = x->backward;
    	    } else {
    	        // 如果x是尾節點,則修改尾結點的指向
    	        zsl->tail = x->backward;
    	    }
    	    // 修改表中最大level值
    	    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
    	        zsl->level--;
    	    // 節點x已經不再關聯,長度減1
    	    zsl->length--;
    	}
    
    

API

參考之《Redis設計與實現》

函數作用
zskiplist *zslCreate(void)創建一個新的跳躍表
void zslFree(zskiplist *zsl)釋放給定跳躍表,以及表中包含的所有節點
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele)將包含給定成員和分值的新節點添加到跳躍表中
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node)刪除跳躍表中包含給定成員和分值的節點
unsigned long zslGetRank(zskiplist *zsl, double score, sds o)返回包含給定成員和分值的節點在跳躍表中的排位
zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank)返回跳躍表在給定排位上的節點
int zslIsInRange(zskiplist *zsl, zrangespec *range)給定一個分值範圍range,如果跳躍表中有至少一個節點的分值在這個分值範圍內,返回1,否則返回0。zrangespec是包含minmax的結構體。
zskiplistNode *zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range)給定一個分值範圍range,返回跳躍表中第一個符合這個範圍的節點
zskiplistNode *zslLastInRange(zskiplist *zsl, zlexrangespec *range)給定一個分值範圍range,返回跳躍表中最後一個符合這個範圍的節點
unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, dict *dict)給定一個分值範圍,刪除跳躍表中所有這個範圍內的節點(同時也會刪除dict中對應的key)
unsigned long zslDeleteRangeByRank(zskiplist *zsl, unsigned int start, unsigned int end, dict *dict)給定一個排位範圍,刪除跳躍表中所有這個範圍內的節點(同時也會刪除dict中對應的key)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章