有序集合在咱們的平常生活中很是常見,好比根據成績對學生進行排名、根據得分對遊戲玩家進行排名等。對於有序集合的底層實現,咱們可使用數組、鏈表、平衡樹等結構。數組不便於元素的插入和刪除;鏈表的查詢效率低,須要遍歷全部元素;平衡樹或者紅黑樹等結構雖然效率高但實現複雜。html
所以,Redis 採用了一種新的數據結構——跳躍表。跳躍表的效率堪比紅黑樹,可是其實現卻遠比紅黑樹簡單。node
下面就開始咱們今天的學習之旅!git
按照慣例,咱們將下文中的涉及到的一些概念,在這裏作一下簡單介紹,方便後面的學習。github
0x
是一種標識,用來表示 16 進制。redis
F 是 16 進制中的 15,其二進制表示爲 1111
。算法
FFFF 即 1111 1111 1111 1111
。數組
&0xFFFF
即與 0xFFFF
作位運算,只取低 16 位。markdown
跳躍表是 zset
(有序集合)的基礎數據結構。跳躍表能夠高效的保持元素有序,而且實現相對平衡樹簡單、直觀。Redis 的跳躍表是基於 William Pugh 在 《Skip lists: a probabilistic alternative to balanced trees》 中描述的算法實現的。作了一下幾點改動:數據結構
zrevrange
等命令。skiplist,首先它是一個 list。實際上,它是在有序鏈表的基礎上發展起來的。dom
咱們先來看一下有序鏈表,有序鏈表是全部元素以遞增或遞減方式有序排列的數據結構,其中每一個節點又有指向下個節點的 next 指針,最後一個節點的 next 指針指向 NULL。遞增有序鏈表示例以下:
如圖所示,若是咱們想要查詢值爲 61 的元素,咱們須要從第一個元素開始依次向後查找、比較才能夠找到,查找的順序爲 1 -> 11 -> 21 -> 31 -> 41 -> 51 -> 61,共 7 次比較,時間複雜度爲 O(N)。有序鏈表的插入和刪除操做都須要先找到合適的位置再修改 next 指針,修改操做基本不消耗時間,因此插入、刪除、修改有序鏈表的耗時主要在查找元素上。
假如咱們 每相鄰兩個節點增長一個指針,讓指針指向下下節點,以下圖所示:
新增長的指針連成了一個新的鏈表,可是它包含的節點個數只有原來的一半(1,21,41,61)。如今當咱們想要查找 61 的時候,咱們就沿着這個新鏈表進行查找(綠色指針方向)。查找的順序爲 1 -> 21 -> 41 -> 61,共 4 次比較,須要比較的次數大概只有原來的一半。
利用一樣的方式,咱們能夠在上層新產生的鏈表上,繼續爲每相鄰的兩個節點增長一個指針,從而查看第三層鏈表,以下圖所示:
新增長的指針連成了一個新的鏈表,它包含的節點個數只有第二層的一半(1,41)。如今當咱們想要查找 61 的時候,咱們沿着新鏈表進行查找(紅色指針方向)。查找順序爲 1 -> 41,此時咱們發現 41 的 next 指針指向 null,咱們就開始從 41 節點的下一層開始查找(綠色指針方向),即 41 -> 61,連起來就是 1-> 41 -> 61,總共比較了 3 次,相比於上次查找又少了一次。當數據量大的時候,這種優點會更加明顯。
skiplist 正是受這種 多層鏈表 的想法啓發設計得來的。
按照上面生成鏈表的方式,上面每一層鏈表的節點個數,是下面一層的節點個數的一半,這樣查找過程就很是相似於一個 二分查找,使得查找的時間複雜度能夠降到 O(logN)。
可是新插入一個節點以後,就會打亂上下相鄰兩層鏈表上節點個數嚴格的 2:1 的對應關係。若是要維持這種對應關係,就必須把新插入的節點後面的全部節點(也包括新插入的節點)從新進行調整,這會讓時間複雜度從新退化爲 O(N)。刪除數據也有一樣的問題。
skiplist 爲了不這一問題,它 不要求上下相鄰兩層鏈表之間的節點個數有嚴格的對應關係,而是爲每一個節點隨機出一個層數(level),新插入的節點就會根據本身的層數決定該節點是否在這層的鏈表上。
從上面咱們能夠知道,跳躍表由多個節點構成,每一個節點由不少層構成,每層都有指向本層的下個節點的指針。
跳躍表主要涉及 server.h 和 t_zset.c 兩個文件,其中在 server.h 中定義了跳躍表的數據結構,在 t_zset.c 中定義了跳躍表的節本操做。
接下來,讓咱們一塊兒來看一下跳躍表具體是如何實現的。
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
複製代碼
該結構體包含以下屬性:
level
數組中的 每項 包含如下兩個元素:
跳躍表是 Redis 有序集合的底層實現方式之一。因此每一個節點的 ele 存儲有序集合的成員 member 值,score 存儲成員 score 值。全部節點的分值是按從小到大的方式排序的,當有序集合的成員分值相同時,節點會按 member 的字典序進行排序。
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
複製代碼
該結構體中包含以下屬性:
經過跳躍表結構體的屬性咱們能夠看到,程序能夠在 O(1) 的時間複雜度下,快速獲取到跳躍表的頭結點、尾節點、長度和高度。
咱們已經知道了跳躍表節點和跳躍表結構體的定義,接下來咱們再看一下跳躍表的建立、插入、查找和刪除操做。
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl)); //初始化內存空間
zsl->level = 1; //將層數設置爲最小的 1
zsl->length = 0; //將長度設置爲 0
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); //建立跳躍表頭節點,層數爲 ZSKIPLIST_MAXLEVEL=64 層
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) { //依次給頭節點的每層賦值
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
zsl->header->backward = NULL; //頭節點的回退指針設置爲 NULL
zsl->tail = NULL; //尾節點設置爲 NULL
return zsl;
}
複製代碼
能夠看到,跳躍表的建立過程以下:
首先聲明一塊大小爲 sizeof(zskiplist)
的內存空間。
而後將層高 level
設置爲 1,將跳躍表長度 length
設置爲 0。而後建立頭節點 header
,其中 ZSKIPLIST_MAXLEVEL
的定義以下:
#define ZSKIPLIST_MAXLEVEL 64
複製代碼
表明層節點最高爲 64 層,而咱們的頭結點正是最高的層數。
頭節點是一個特殊的節點,不存儲有序集合的 member
信息。頭節點是跳躍表中第一個插入的節點,其 level
數組的每項 forward
都 爲NULL,span
值都爲 0。
接着將頭節點的回退指針 backward
和尾指針 tail
設置爲 NULL。
這些都很好理解,就是初始化內存,而後依次將跳躍表結構體各個成員設置默認值。
建立跳躍表節點代碼以下:
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
zskiplistNode *zn =
zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel)); //申請 zskiplistNode + 柔型數組(多層)大小的空間
zn->score = score; //設置節點分支
zn->ele = ele; //設置節點數據
return zn;
}
複製代碼
建立跳躍表節點的代碼也很好理解。
首先分配內存空間,這個空間大小爲 zskiplistNode 的大小和 level 數組的大小。
zskiplistNode
結構體的最後一個元素爲柔性數組,申請內存時須要指定柔性數組的大小,一個節點佔用的內存大小爲 zskiplistNode
的內存大小與 level
個 zskiplistLevel
的內存大小之和。
再將節點的 score
和 ele
分別賦值。
插入節點這塊比較重要,也比較難懂,咱們仔細學習一下。
首先附上插入節點代碼。
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; // update[] 數組用於存儲被插入節點每層的前一個節點
unsigned int rank[ZSKIPLIST_MAXLEVEL]; // rank[] 數組記錄當前層從 header 節點到 update[i] 節點所經歷的步長。
int i, level;
serverAssert(!isnan(score));
x = zsl->header; //遍歷的節點,因爲查找被插入節點每層的前一個節點
for (i = zsl->level-1; i >= 0; i--) { //從上到下遍歷
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; //給rank[] 數組初始值賦值,最上層從 header 節點開始,因此爲 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))) //前進的規則存在 forward 節點且(forward 節點評分小於待插入節點評分 || (forward 節點評分等於待插入節點評分 && forward 節點元素字典值小於待插入節點元素字典值))
{
rank[i] += x->level[i].span; //加上 x 的跨度
x = x->level[i].forward; //節點向前前進
}
update[i] = x; // 將被插入節點當前層的前一個節點記錄在 update[] 數組中
}
level = zslRandomLevel(); //隨機生成一個層高
if (level > zsl->level) { //新生成節點的層高比當前跳躍表層高大事
for (i = zsl->level; i < level; i++) { //只更新高出的部分
rank[i] = 0; //由於是頭結點,因此爲 0
update[i] = zsl->header; //該層只有頭結點
update[i]->level[i].span = zsl->length; //由於 forward 指向 NULL,因此跨度應該是跳躍表全部的節點,因此 span 爲跳躍表的長度
}
zsl->level = level; //更新跳躍表的層高
}
x = zslCreateNode(level,score,ele); // x 被賦值成新建立的節點
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward; //更新 x 節點的 level[i] 層的 forward 指針
update[i]->level[i].forward = x; //更新 update[i] 節點的 level[i] 層的 forward 指針
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); //更新 x 節點的 level[i] 層的跨度 span
update[i]->level[i].span = (rank[0] - rank[i]) + 1; //更新 update[i] 節點的 level[i] 層的跨度 span
}
for (i = level; i < zsl->level; i++) { //當新插入節點的層高比跳躍表的層高小時,須要更新少的幾層的 update[] 節點的跨度,即 +1
update[i]->level[i].span++;
}
x->backward = (update[0] == zsl->header) ? NULL : update[0]; //更新 x 的 backward 指針,若是 update[0] 是頭結點則爲 NULL,不然爲 update[0]
if (x->level[0].forward) // 更新 x 節點第 0 層有後續節點,則後面節點的 backward 指向 x 節點,不然的話 x 節點爲最後一個節點,須要將 tail 指針指向 x 節點
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++; //跳躍表的長度 +1
return x;
}
複製代碼
咱們來看一下跳躍表實現過程示意圖:
假設咱們想插入一個節點 45。咱們首先須要找到插入的位置,而後再更改由於節點插入致使受影響的位置,好比跳躍表的 level,前一個節點的每層的 forward 指針等等。
在下圖中,我用紅色標出哪些位置受了影響須要修改。
所以咱們把插入節點的步驟總爲以下幾點:
如今咱們來思考以下幾個問題:
爲何須要先查找要插入的位置,而後再調整跳躍表的高度?
由於咱們是根據跳躍表高度來查找節點的,首先咱們要找到最高的一層,而後一層一層向下查找,直到找到節點。當新插入的節點的 level 比跳躍表的 level 大的時候,若是先調整跳躍表高度,而後咱們就會以調整後的高度爲起點,而後向後查找,可是該層的 forward 指針指向 NULL,咱們是找不到節點的。
如何調整受影響節點和新插入節點每層的 forward 指針和 span?
按照上述思路,接下來讓咱們逐步研究插入節點代碼。
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
複製代碼
定義兩個數組:
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) { //從最高層開始向下遍歷
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;
}
複製代碼
按照上述代碼邏輯,值爲 2五、3五、45 的節點查找插入位置的查找路線以下圖所示:
接下來咱們一步一步分析代碼。
for (i = zsl->level-1; i >= 0; i--)
複製代碼
for 循環的起始值爲 zsl->level-1
,正驗證了上面咱們所說的,節點查詢要從最高層開始查找,查找不到再從下一層開始查詢。
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
複製代碼
rank[]
數組的做用是記錄當前層從 header 節點到 update[i] 節點所經歷的步長。
從上圖咱們能夠看到,節點查找路線是 「右->下->右->下」 這種的。
在最高層的時候,咱們的起始位置確定是 header
節點,此時該節點與 header 節點之間的距離爲 0,因此 rank[zsl->level-1]
的值確定爲 0。
當咱們向下層走的時候,其實是從上面一層查到的最後一個節點下來的,好比上圖中查找值爲 45 的節點的時候,當咱們從第四層下到第三層的時候,是從 41 節點開始查的,rank[2] 的值同第四層的值 rank[3]。
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;
複製代碼
這段代碼說明了咱們尋找節點插入位置的兩條比較原則:
即咱們提到的,評分不相等時比較評分,評分相等值比較值的字典排序,不會出現兩個都相等的狀況。
接着記錄步長 rank[i]
,rank[i]
的值即爲當前節點的步長 rank[i]
加上該節點到下一節點的跨度 x->level[i].span
。
節點向前移動到下一個節點。
當一層走完循環以後,此時應該知足兩種狀況:
x->forward == NULL
x->forward != NULL && (x->forward.score > score || (x->forward.score == score && sdscmp(x->level[i].forward->ele,ele) > 0))
此時咱們應該向從下一層開始尋找了,那麼咱們應該記住受影響的節點,也是插入節點每層的前一個節點 update[i] = x
。
循環直到第一層結束,此時咱們已經找到了要插入的位置,並將插入節點每層的前一個節點記錄在 update[]
數組中,並將 update[]
數組中每一個節點到 header 節點所經歷的步長也記錄了下來。
咱們以 length=3 level=2 的一個跳躍表插入節點爲例,update 和 rank 賦值後跳躍表以下:
level = zslRandomLevel();
複製代碼
每一個節點的層高是隨機生成的,即所謂的 機率平衡,而不是 強制平衡,所以,對於插入和刪除節點比傳統上的平衡樹算法更爲簡潔高效。
生成方法以下:
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) //ZSKIPLIST_P=0.25
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
複製代碼
上述代碼中,level 的初始值爲 1,經過 while 循環,每次生成一個隨機值,取這個值的低 16 位做爲 x,當 x 小於 0.25 倍的 0XFFFFFF 時,level 的值加 1;不然退出 while 循環,最終返回 level 和 ZSKIPLIST_MAXLEVEL 二者中的最小值。
下面計算節點的指望層高。假設 p = ZSKIPLIST_P;
因此節點的指望層高爲:
當 p=0.25 時,跳躍表節點的指望層高爲 1/(1-0.25)≈1.33。
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;
}
複製代碼
只有當待插入節點的層高比當前跳躍表的層高大時,纔會進行該操做。
zsl->level = level;
跳躍表的層高賦值爲最高的層高,這是沒有問題的。
咱們接着以該圖爲例:
第 0 層和第 1 層咱們已經更新過了,所以咱們只須要從未更新過的層開始便可,即 i = zsl->level;
,從第 2 層開始。第 2 層只須要更新 header 節點,因此 update[i] = zsl->header
。而 rank[i]
則爲 0。
update[2]->level[2].span
的值先賦值爲跳躍表的總長度,後續在計算新插入節點 level[2]
的 span
時會用到此值。在更新完新插入節點 level[2]
的 span
以後會對 update[2]->level[2].span
的值進行從新計算賦值。
至於爲何將 update[2]->level[2].span
的值設置爲跳躍表的總長度,咱們能夠從 span
的定義來思考。span
的含義是 forward 指向的節點與本節點之間的元素個數。而 update[2]->level[2].forward
指向的是 NULL 節點,中間隔着的是跳躍表的全部節點,所以賦值爲跳躍表的總長度。
調整高度後的跳躍表以下圖所示:
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;
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
複製代碼
forward 值的修改很好理解,就是簡單的鏈表插入節點。
那麼如何理解 update[i]->level[i].span - (rank[0] - rank[i])
和 (rank[0] - rank[i]) + 1
呢?
咱們對照下圖來深刻理解一下 span 賦值過程。
首先,咱們應該對一下幾點有所瞭解:
咱們以 update[1] 節點舉例,其餘節點原理也是如此。
因此,rank[0] - rank[1]
實際上就是節點 update[1]
到 update[0]
的距離(score=1 的節點到 score=21 的節點的距離)
update[1]->level[1].span
的值表示在第一層 update[1] 節點與指向的節點之間的跨度,從上圖咱們能夠看到,這段距離中包含 update[1]
到 update[0]
的距離,剩下的距離就是 新插入節點到 update[1]->level[1].forward
節點之間的距離。
由於新插入的節點是在 update[0] 後面插入的,所以 update[0]
和 新插入節點 之間的距離爲 1,rank[0] - rank[1] + 1
即爲 update[1]->level[1].span
的值。
咱們把問題抽象化一下:
假設有節點 A 和 B,在他們中間插入 X,
rank[0] - rank[i]
計算的就是 A 到 X 的前一個節點 X-1 的距離;update[i]->level[i].span
計算的就是 A 到 B 的距離;update[i]->level[i].span - (rank[0] - rank[i])
計算的就是 X 到 B 的距離。update[i]->level[i].span = (rank[0] - rank[i]) + 1
計算的是 A 到 X-1 再 +1,表示的是 A 到 X 的距離。計算的原則是 左開右閉。
按照上述算法,咱們來實際走一遍插入過程。level 的值爲 3,因此能夠執行三次 for 循環,插入過程以下:
第一次 for 循環
插入節點並更新第 0 層後的跳躍表以下圖所示:
第二次 for 循環
插入節點並更新第 1 層後的跳躍表以下圖所示:
第三次 for 循環
插入節點並更新第 2 層後的跳躍表以下圖所示:
新插入節點的高度大於原跳躍表高度,因此下面代碼不會運行。但若是新插入節點的高度小於原跳躍表高度,則從 level 到 zsl->level-1 層的 update[i] 節點 forward 不會指向新插入的節點,因此不用更新 update[i] 的 forward 指針,只將這些 level 層的 span 加 1 便可。
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;
複製代碼
根據 update 的賦值過程,新插入節點的前一個節點必定是 update[0],因爲每一個節點的後退指針只有一個,與此節點的層數無關,因此當插入節點不是最後一個節點時,須要更新被插入節點的 backward 指向 update[0]。若是新插入節點是最後一個節點,則須要更新跳躍表的尾結點爲新插入節點。插入及誒單後,更新跳躍表的長度加 1.
插入新節點後的跳躍表以下圖所示:
有了上面插入節點的學習,對於節點的刪除,咱們應該更容易理解了。
咱們把刪除節點簡單的分爲兩步:
刪除節點代碼以下:
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
int i;
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) { // update[i].level[i] 的 forward 節點是 x 的狀況,須要更新 span 和 forward
update[i]->level[i].span += x->level[i].span - 1;
update[i]->level[i].forward = x->level[i].forward;
} else {// update[i].level[i] 的 forward 節點不是 x 的狀況,只須要更新 span
update[i]->level[i].span -= 1;
}
}
if (x->level[0].forward) { // 若是 x 不是尾節點,更新 backward 節點
x->level[0].forward->backward = x->backward;
} else { // 不然 更新尾節點
zsl->tail = x->backward;
}
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--; //更新跳躍表 level
zsl->length--; // 更新跳躍表長度
}
複製代碼
查找須要刪除的節點要藉助 update 數組,數組的賦值方式與 插入節點 中的 update 的賦值方式相同,再也不贅述。查找完畢以後,update[2]=header,update[1] 爲 score=1 的節點,update[0] 爲 score=21 的節點。刪除節點前的跳躍表以下圖所示:
設置 span 和 forward 的代碼以下:
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;
}
}
複製代碼
咱們先來看 span 的賦值過程。刪除節點時 span 的賦值以下圖所示:
假設咱們想要刪除 score=21 的節點,那麼 update[0] 和 update[1] 應該爲 score=1 的節點,update[2] 應該爲頭節點。
現更新節點的 span 和 forward 分爲兩種狀況:
update[i] 的第 i 層的 forward 節點指向 x(如上圖 update[0]->level[0])
update[0].level[0].span
是 update[0] 到 x 的距離;x.level[0].span
是 x 到 x.level[0].forward 之間的距離;update[0].level[0].span + x.level[0].span
是 update[0] 到 x.level[0].forward 之間的距離;update[0].level[0].span + x.level[0].span - 1
是刪除 x 節點後 update[0] 到 x.level[0].forward 之間的距離;update[0].level[0].forward
即爲 x.level[0].forward。update[i] 的第 i 層的 forward 節點指向 x(如上圖 update[1]->level[1])
設置 span 和 forward 後的跳躍表以下圖所示:
update 節點更新完畢以後,須要更新 backward 指針、跳躍表高度和長度、若是 x 不爲最後一個節點,之間將第 0 層後一個節點的 backward 賦值爲 x 的backward 便可;不然,將跳躍表的尾指針指向 x 的 backward 節點便可。代碼以下:
if (x->level[0].forward) {
x->level[0].forward->backward = x->backward;
} else {
zsl->tail = x->backward;
}
複製代碼
當刪除的 x 節點是跳躍表的最高節點,而且沒有其餘節點與 x 節點的高度相同時,須要將跳躍表的高度減 1。
因爲刪除了一個節點,跳躍表的長度須要減 1。
刪除節點後的跳躍表以下圖所示:
刪除跳躍表就比較簡單了。獲取到跳躍表對象以後,從頭節點的第 0 層開始,經過 forward 指針逐步向後遍歷,沒遇到一個節點便將其釋放內存。當全部節點的內存都被釋放以後,釋放跳躍表對象,即完成了跳躍表的刪除操做。代碼以下
void zslFree(zskiplist *zsl) {
zskiplistNode *node = zsl->header->level[0].forward, *next;
zfree(zsl->header);
while(node) {
next = node->level[0].forward;
zslFreeNode(node);
node = next;
}
zfree(zsl);
}
複製代碼
在 Redis 中,跳躍表主要應用於有序集合的底層實現(有序集合的另外一種實現方式爲壓縮列表)。
在 redis.conf 有關於有序集合底層實現的兩個配置:
zset-max-ziplist-entries 128 // zset 採用壓縮列表時,元素個數最大值。默認值爲 128。
zset-max-ziplist-value 64 // zset 採用壓縮列表時,每一個元素的字符串長度最大值,默認爲 64。
複製代碼
zset 添加元素的主要邏輯位於 t_zset.c 的zaddGenericCommand
函數中。zset 插入第一個元素時,會判斷下面兩種條件:
zset-max-ziplist-entries
的值是否等於 0;zset-max-ziplist-value
小於要插入元素的字符串長度。知足任一條件 Redis 就會採用跳躍表做爲底層實現,不然採用壓縮列表做爲底層實現方式。
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
zobj = createZsetObject(); //建立跳躍表結構
} else {
zobj = createZsetZiplistObject(); //建立壓縮列表結構
}
複製代碼
通常狀況下,不會將 zset_max_ziplist_entries
配置成 0,元素的字符串長度也不會太長,因此在建立有序集合時,默認是有壓縮列表的底層實現。zset 新插入元素時,會判斷如下兩種條件:
zset_max_ziplist_entries
;zset_max_ziplist_value
。當慢如任一條件時,Redis 便會將 zset 的底層實現由壓縮列表轉爲跳躍表,代碼以下:
if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries ||
sdslen(ele) > server.zset_max_ziplist_value)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
複製代碼
值得注意的是,zset 在轉爲跳躍表以後,即便元素被逐漸刪除,也不會從新轉爲壓縮列表。
本章咱們介紹了跳躍表的演變過程、基本原理、和實現過程。
演變過程就是在鏈表的基礎上,間隔抽取一些點,在上層造成一個新的鏈表,相似於二分法,達到時間減半的效果,可是又不一樣於二分法,由於新插入的節點的層高是隨機生成的,即所謂的 機率平衡,這樣保證了跳躍表的查詢、插入、刪除的平均複雜度都爲 O(logN)。
跳躍表的實現過程,咱們着重講了插入節點,其中咱們引入了兩個數組,update[] 和 rank[] 數組,咱們須要對這兩個數組特別理解,才能理解插入過程。
看到這了,咱們不妨問本身幾個問題:
若是你們可以對這些問題解答出來,相信你們已經對跳躍表瞭如指掌了。