筆者大三,最近複習到了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的具體結構是這樣的:
整張表的基本結構是這樣的:
二,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;
}
大概邏輯:
- 從最高索引層開始遍歷,根據 score 找到它的前驅節點,用 update 數組進行保存
- 每一層得進行節點的插入,並計算更新 span 值
- 修改 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層,就形成了一個雙向鏈表,從而可以方便的從表尾向表頭遍歷