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 中跳躍表的內部機制和實現細節。