從redis源碼看數據結構(四)跳躍鏈表

筆者大三,最近複習到了redis,如有錯誤,還請及時指出

從redis源碼看數據結構(四)跳躍鏈表

一,redis中的跳錶

redis 中的有序集合是由我們之前介紹過的字典加上跳錶實現的,字典中保存的數據和分數 score 的映射關係,每次插入數據會從字典中查詢key,如果已經存在了,就不再插入,有序集合中是不允許重複數據。

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

在這裏插入圖片描述

1.底層結構體

typedef struct zskiplist {

    // 表頭節點和表尾節點(指向的是最底層鏈表的頭結點和尾節點)
    struct zskiplistNode *header, *tail;

    // 表中節點的數量
    unsigned long length;

    // 表中層數最大的節點的層數
    int level;

} zskiplist;

跳錶節點

typedef struct zskiplistNode {

    // 後退指針
    struct zskiplistNode *backward;

    // 分值,最底層鏈表是按照分支大小,進行排序串聯起來的
    double score;

    // 成員對象
    //這裏的redis版本是4.0,以前是redisObject類型,現在是sds類型,即現在跳錶只用於存儲字符串數據
   sds ele;

    //每個節點除了儲存節點自身數據外,還通過level數組保存了該節點在整個跳錶各個索引層的節點引用
    // 層
    struct zskiplistLevel {

        // 前進指針
        struct zskiplistNode *forward;

        // 跨度,跨過多少個節點
        unsigned int span;

    } level[];

} zskiplistNode;

關於zskiplistLevel的具體結構是這樣的:

image

整張表的基本結構是這樣的:

image

二,redis中跳躍鏈表的操作

1.創建跳錶

/*
 * 創建一個跳躍表
 *
 * T = O(1)
 */
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    zsl = zmalloc(sizeof(*zsl));
	//默認一層索引層
    zsl->level = 1;
    //初始化時沒有節點
    zsl->length = 0;

    // 初始化頭節點, O(1),給其分配32個索引層,即redis中索引層最多32層ZSKIPLIST_MAXLEVEL = 32
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    // 初始化層指針,O(1)
    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;
}

redis 中實現的跳錶最高允許 32 層索引,這麼做也是一種性能與內存之間的衡量,過多的索引層必然佔用更多的內存空間,32 是一個比較合適值。

2.插入一個節點

/*
 * 將包含給定 score 的對象 obj 添加到 skiplist 裏
 *
 * T_worst = O(N), T_average = O(log N)
 */
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {

    //update數組將用於記錄新節點在每一層索引的目標插入位置
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;

    // 記錄尋找元素過程中,每層所跨越的節點數
    unsigned int rank[ZSKIPLIST_MAXLEVEL];

    int i, level;

    redisAssert(!isnan(score));
    x = zsl->header;
    //這一段就是遍歷每一層索引,找到最後一個小於當前給定score值的節點,保存在update數組中 
    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 &&                   
            // 右節點的 score 比給定 score 小
            (x->level[i].forward->score < score ||      
                // 右節點的 score 相同,但節點的 member 比輸入 member 要小
                (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;
    }

    //至此,update數組中已經記錄好,每一層最後一個小於給定score值的節點
    
    // 計算新的隨機層數
    level = zslRandomLevel();
    // 如果 level 比當前 skiplist 的最大層數還要大
    //爲高出來的索引層賦初始值,update[i]指向哨兵節點,想構造跳躍鏈表一樣
    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,obj);
    // 根據 update 和 rank 兩個數組的資料,初始化新節點
    // 並設置相應的指針
    // O(N)
    for (i = 0; i < level; i++) {
        //原: update[i]->level[i] -> update[i]->level[i].forward
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;
        //後: update[i]->level[i] -> x -> update[i]->level[i].forward

        //rank[0]等於新節點再最底層鏈表的排名,就是它前面有多少個節點
        //update[i]->level[i].span記錄的是目標節點與後一個索引節點之間的跨度,即跨越了多少個節點
        //得到新插入節點與後一個索引節點之間的跨度
        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 */
    // 更新沿途訪問節點的 span 值
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }

    // 設置後退指針
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    // 設置 x 的前進指針
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        // 這個是新的表尾節點
        zsl->tail = x;

    // 更新跳躍表節點數量
    zsl->length++;

    return x;
}

大概邏輯:

  1. 從最高索引層開始遍歷,根據 score 找到它的前驅節點,用 update 數組進行保存
  2. 每一層得進行節點的插入,並計算更新 span 值
  3. 修改 backward 指針與 tail 指針

3.刪除節點

/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */
/*
 * 節點刪除函數
 *
 * T = O(N)
 */
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;

    // 修改相應的指針和 span , O(N)
    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;
        }
    }

    // 處理表頭和表尾節點
    if (x->level[0].forward) {
        x->level[0].forward->backward = x->backward;
    } else {
        zsl->tail = x->backward;
    }

    // 收縮 level 的值, O(N)
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
        zsl->level--;

    zsl->length--;
}

三,redis中的跳錶和普通跳錶的區別

redis的跳錶和普通的跳錶實現沒有多大區別,主要區別在三處:

  • redis的跳錶引入了score,且score可以重複

  • 排序不止根據分數,還可能根據成員對象(當分數相同時)

  • 有一個前繼指針,因此在第1層,就形成了一個雙向鏈表,從而可以方便的從表尾向表頭遍歷

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