Redis資料結構—跳躍表 skiplist 實現原始碼分析

威哥爱编程發表於2024-07-12

Redis 是一個開源的記憶體資料結構儲存系統,它可以用作資料庫、快取和訊息中介軟體。Redis 的資料結構非常豐富,其中跳躍表(skiplist)是一種重要的資料結構,它被用來實現有序集合(sorted sets)。

跳躍表是一種機率型資料結構,它透過多層連結串列來實現快速的查詢操作。跳躍表的結構類似於多層索引,每一層都是一個有序連結串列,但每一層的連結串列節點數量逐漸減少,最頂層的連結串列節點最少,最底層的連結串列節點最多。這樣設計的好處是,可以在對數時間內完成查詢操作,同時插入和刪除操作也非常高效。

跳躍表的主要特點包括:

  • 有序性:跳躍表中的元素是有序的,可以快速地進行範圍查詢。
  • 機率性:跳躍表的高度是隨機決定的,這使得它在平均情況下具有對數時間複雜度。
  • 動態性:跳躍表可以在執行時動態地新增和刪除元素,而不需要重新構建整個結構。
  • 空間效率:相比於平衡樹,跳躍表的空間效率更高,因為它不需要儲存指向父節點的指標。

在 Redis 中,跳躍表被用於實現有序集合,它允許使用者新增、刪除、更新和查詢元素,並且可以按照分數對元素進行排序。跳躍表的實現細節在 Redis 原始碼中可以找到,它是 Redis 高效性的關鍵因素之一。

以下是根據 Redis 原始碼對其實現原理的詳細分析:

資料結構定義:
Redis 中的跳躍表由 zskiplistNode 和 zskiplist 兩個結構體定義。zskiplistNode 表示跳躍表的節點,包含成員物件 obj、分值 score、後退指標 backward 以及層 level 資訊;zskiplist 表示跳躍表本身,包含頭尾節點指標、長度和層高資訊。

節點層級:
跳躍表的每個節點可以有多個層,稱為索引層,每個索引層包含一個前向指標 forward 和跨度 span。層高是隨機生成的,遵循冪次定律,最大層高為 32。

建立跳躍表:
使用 zslCreate 函式建立一個新的跳躍表,初始化層高為 1,長度為 0,並建立頭節點,頭節點的層高為 32,其餘節點的層高根據需要動態生成。

插入節點:
插入操作首先確定新節點的層高,然後從高層向低層搜尋插入位置,並更新 update 陣列,該陣列記錄所有需要調整的前置節點。接著,建立新節點,並根據 update 陣列和 rank 陣列更新跨度和前向指標。

查詢操作:
查詢操作從高層開始,沿著連結串列前進;遇到大於目標值的節點時下降到下一層,繼續查詢。經過的所有節點的跨度之和即為目標節點的排位(rank)。

刪除節點:
刪除操作根據分值和物件找到待刪除節點,並更新相關節點的前向指標和跨度。如果節點在多層中存在,需要逐層刪除。

效能分析:
跳躍表支援平均 O(logN)、最壞 O(N) 複雜度的節點查詢,且實現比平衡樹簡單。在有序集合中,跳躍表可以處理元素數量較多或元素成員較長的情況。

Redis 應用場景:
Redis 使用跳躍表實現有序集合鍵,特別是當集合中的元素數量較多或元素的成員是較長的字串時。跳躍表也用於 Redis 叢集節點中的內部資料結構。

跳躍表的優點:
跳躍表的優點包括支援快速的查詢操作,以及在實現上相對簡單。它透過維護多個層級的連結串列來提高查詢效率,且可以順序性地批次處理節點。

跳躍表的實現細節:
跳躍表的實現細節包括節點的建立、插入、刪除、搜尋等操作,以及維護跳躍表的最大層高和節點數量等資訊。具體實現可以參考 Redis 原始碼中的 t_zset.c 檔案。

Redis 的跳躍表實現是對其有序集合效能和功能的重要支撐,透過上述分析,我們可以更深入地理解這一資料結構的內部機制。

結合 Redis 原始碼中的跳躍表實現,我們可以深入理解其工作原理。以下是根據 Redis 原始碼中的跳躍表實現程式碼進行的分析:

跳躍表節點定義 (zskiplistNode)

typedef struct zskiplistNode {
    robj *obj; // 指向成員物件的指標
    double score; // 分數值
    struct zskiplistNode *backward; // 後退指標,用於從後往前遍歷
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 前進指標
        unsigned int span; // 跨度,表示該層跨越的元素數量
    } level[]; // 索引層,包含多個索引
} zskiplistNode;

跳躍表定義 (zskiplist)

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 頭尾節點指標
    unsigned long length; // 跳躍表的長度,即元素數量
    int level; // 跳躍表的最大層數
} zskiplist;

跳躍表的建立 (zslCreate)

zskiplist *zslCreate(void) {
    int j;
    zskiplist *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;
}

跳躍表的插入 (zslInsert)


zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
    // ...
    // 1. 初始化更新陣列和 rank 陣列
    // 2. 從高層向底層搜尋,找到每個層級的插入位置
    // 3. 確定新節點的層數
    // 4. 建立新節點,並更新前向指標和跨度
    // 5. 更新跳躍表的最大層數和長度
    // ...
}

跳躍表的搜尋 (zslGetRank)


unsigned long zslGetRank(zskiplist *zsl, double score, robj *o, int reverse) {
    // ...
    // 1. 從高層向底層搜尋目標元素
    // 2. 累加跨度以計算元素的排名
    // ...
}

跳躍表的刪除 (zslDelete)


zskiplistNode *zslDelete(zskiplist *zsl, double score, robj *obj) {
    // ...
    // 1. 搜尋目標元素並記錄需要更新的節點
    // 2. 逐層刪除節點
    // 3. 更新跨度和前向指標
    // 4. 如果刪除了頭節點,更新頭節點
    // ...
}

跳躍表的高度隨機化

Redis 中節點的層高是隨機決定的,通常使用固定機率(如 1/2)來確定。但在 Redis 實現中,節點的層高是根據冪次定律隨機生成的,介於 1 和 32 之間。

總結

Redis 的跳躍表實現涉及多個關鍵操作:建立、插入、搜尋和刪除。每個操作都需要對節點的層級和跨度進行精確管理,以保證跳躍表的有序性和高效的查詢效能。跳躍表的高度隨機化和層級結構的設計使得 Redis 能夠在對數時間內完成查詢操作,同時保持了較高的空間效率和動態性。透過原始碼分析,我們可以更深入地理解 Redis 中跳躍表的內部機制和實現細節。

相關文章