Redis底層探秘(二):鏈表和跳躍表


 

鏈表簡介

       鏈表提供了高效的節點重排能力,以及順序性的節點訪問方式,並且可以通過增刪節點來靈活地跳轉鏈表的長度。

       作為一種常用數據結構,鏈表內置在很多高級的編程語言里面,因為Redis使用C語言並沒有內置這種數據結構,所以Redis構建了自己的鏈表實現。

       鏈表在Redis中的應用非常多,比如列表鍵的底層實現之一就是鏈表,發布與訂閱、慢查詢、監視器等功能也用到了鏈表,Redis服務器本身還是用鏈表來保存多個客戶端的狀態信息,以及使用鏈表來構建客戶端輸出緩沖區(output buffer)。

鏈表和鏈表節點的實現

每個鏈表的節點都是一個adlist.h/ listNode結構

typedef struct listNode {
    struct listNode *prev;//前置節點
    struct listNode *next;//后置節點
    void *value;//節點的值
} listNode;

 

       雖然使用多個listNode結構就可以組成鏈表,但使用adlist.h/ list來持有鏈表的話,操作起來會更方便。

typedef struct list {
//表頭節點
listNode *head;
//表尾節點
listNode *tail;
//節點值復制函數
void *(*dup)(void *ptr);
//節點值釋放制函數
void (*free)(void *ptr);
//節點值對比函數
int (*match)(void *ptr, void *key);
//鏈表所包含的節點數量
    unsigned long len;
} list;

Redis的鏈表特性總結

1、 雙端:鏈表節點帶有prev和next指針,獲取某個節點的前置節點和后置節點的復雜度都是O(1)。

2、 無環:表頭節點的prev指針和表位節點的next指針都指向null,對鏈表的訪問以null為終點。

3、 帶表頭指針和表尾指針:通過list結構的head指針和tail指針,程序獲取鏈表的表頭節點和表尾節點的復雜度為O(1)。

4、 帶鏈表長度計數器:程序使用list結構的len屬性來對list持有的鏈表節點進行計數,所以獲取節點數量的復雜度為O(1)。

5、 多態:鏈表節點使用void *指針來保存節點值,並且可以通過list結構的dup、free、match三個屬性為節點設置類型特定函數,所以鏈表可以用於保存各種不同類型的值。

鏈表和鏈表節點的API

 

跳躍表(skipList)簡介

跳躍表(skipList)是一種有序數據結構,他通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。

跳躍表支持評價O(logN)、最壞O(N)復雜度的節點查找,還可以通過順序性操作批量處理節點

在大部分情況下,跳躍表的效率可以和平衡樹相媲美,並且因為跳躍表的實現比平衡樹來得更簡單,所以有不少程序都是用跳躍表來代替平衡樹。

具體關於跳躍表的原理,可查看這個《漫畫算法:什么是跳躍表》http://blog.jobbole.com/111731/

Redis使用跳躍表作為有序結合鍵的底層實現之一,如果一個有序集合包含的元素數量比較多,又或者有序集合中元素的成員時比較長的字符串時,redis就會使用跳躍表來作為有序集合鍵的底層實現。

Redis在兩個地方用到了跳躍表,一個是實現有序集合鍵,另一個是在集群節點中用作內部數據結構。

跳躍表的實現

       Redis的跳躍表由redis.h\zskiplistNode和redis.h/zskiplist兩個結構定義(最新版已經移動到server.h),其中zskiplistNode結構用於表示跳躍表節點,而zskiplist結構則用於保存跳躍表節點的相關信息,比如節點數量,以及指向表頭界定啊和表尾節點的指針等等。

 

上圖最左邊的就是zskiplist結構,該結構包含以下屬性:

1 header:指向跳躍表的表頭表頭節點。

2 tail:指向跳躍表的表尾節點。

3 level:記錄目前跳躍表內,層數最大的那個節點的層數(表頭節點的層數不計算在內)。

4 length:記錄跳躍表的長度,也即是,跳躍表目前包含節點的數量(表頭節點不計算在內)。

       位於zskiplist結構右方的四個zskiplistNode結構,該結構包含一下屬性:

1 層(level):節點中用L1、L2、L3等字樣標記節點的各個層,L1代表第一層,L2代表第二層,以此類推。每個層都帶有兩個屬性:前進指針跨度。前進指針用於訪問位於表尾方向的其他節點,而跨度則記錄了前進指針所指向節點和當前節點的舉例。在上圖中,連線上帶有數字的箭頭就代表前進指針,而那個數字就是跨度。當程序從表頭向表尾進行遍歷時,訪問會沿着層的前進指針進行。

2 后退指針:節點中用BW(backward)字樣標記節點的后退指針,他指向位於當前節點的前一個節點。后退指針在程序從表尾向表頭遍歷時使用。

3 分值(score):各個節點中的1.0、2.0和3.0是節點所保存的分值。在跳躍表中,節點按各自所保存的分值從小到大排量。

4 成員對象(obj):各個節點中o1、o2和o3是節點所保存的成員對象。

       注意:表頭節點和其他節點的構造是一樣的;表頭節點也有后退指針、分值和成員對象,不過表頭節點的這些屬性不會被用到,所以圖中省略了。

跳躍表節點zskiplistNode

跳躍表節點由server.h\zskiplistNode結構定義

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
//成員對象
robj *obj;
//分值
double score;
//后退指針
struct zskiplistNode *backward;
//
    struct zskiplistLevel {
        struct zskiplistNode *forward;//前進指針
        unsigned int span;//跨度
    } level[];
} zskiplistNode;

1、 層

跳躍表節點的level數組可以包含多個元素,每個元素都包含一個指向其他節點的指針,程序可以通這些層來加快訪問其他節點的速度,一般來說,層數越多,訪問其他節點的速度就越快。

每次創建一個新的跳躍表節點的時候,程序都根據冪次定律(power law,越大的數出現的概率越小)隨機生成一個介於1和32之間的值作為level數組的大小,這個大小就是層的“高度”。下圖就是帶有不同層高的節點。

 

2、 前進指針

每個層都有一個指向表尾方向的前進指針,用於從表頭向表尾方向訪問節點。下圖用虛線表示出了程序從表頭向表尾方向,遍歷跳躍表中所有節點的路徑:

 

 

       a 迭代程序首先訪問跳躍表的第一個節點(表頭),然后從第四層的前進指針移動到表中的第二個節點。

       b 在第二個節點時,程序沿着第二層的前進指針移動到表中第三個節點。

       c 在第三個節點時,程序同樣沿着第二層的前進指針移動到表中的第四個節點。

       d 當程序再次沿着第四個節點的前進指針移動式,他碰到一個null,程序知道這時已經到達了跳躍表的表尾,於是結束這次遍歷。

3、跨度

層的跨度用於記錄兩個節點之間的距離:

a 兩個節點之間的跨度越大,他們相距得就越遠。

b 指向null的所有前進指針的跨度都為0,因為他們沒有連向任何節點。

       初看上去,很容易以為跨度和遍歷操作有關,但實際上並不是這樣,遍歷操作只使用前進指針就可以完成了,寬度實際上是用來計算排位(rank)的:在查找某個節點的過程中,將沿途訪問過的所有層的跨度累計起來,得到的結果就是目標節點在跳躍表中的排位。

       舉個例子,下圖用虛線標記了在跳躍表中查找分值為3.0、成員對象為o3的節點時,沿途經歷的層:查找的過程只經過了一個層,並且層的跨度為3,所以目標節點在跳躍表中的排位為3。

 

 再舉個例子,下圖用虛線標記了在跳躍表中查找分值為2.0、成員對象為o2的節點時,沿途經歷的層:在查找節點的過程中,程序經過了兩個跨度為1的節點,因此可以計算出,目標節點在跳躍表中的排位為2。

 

4、后退指針

       節點的后退指針用於從表尾向表頭方向訪問節點:跟可以一次跳過多個節點的前進指針不同,因為每個節點只有一個后退指針,所以每次只能后退至前一個節點。

       下圖用虛線表示了如果從表尾向表頭遍歷跳躍表中的所有節點。

5、分值和成員

       節點的分值(score屬性)是一個double類型的浮點數,跳躍表中的所有節點都按照分值從小到大來排序。

       節點的成員對象是一個指針,他指向一個字符串對象,而字符串對象則保存着一個SDS值。

       在同一個跳躍表中,各個節點保存的成員對象必須是唯一的,但是多個節點保存的分值卻可以是相同的:分值相同的節點按照成員對象在字典中的大小來進行排序,成員對象較小的節點會排在前面(靠近表頭方向),而成員對象較大的節點則會排在后面(靠近表尾的方向)。

       舉個例子,在下圖所示的跳躍表中,三個跳躍表節點都保存了相同的分值10086.0,但保存成員對象o1的節點卻排在保存成員對象o2和o3的節點之前,由順序可知,三個對象在字典中的排序哦o1<=o2<=o3。

 

跳躍表zskiplist

緊靠多個跳躍表節點就可以組成一個跳躍表,如下圖。

但通過使用一個zskiplist結構來持有這些節點,程序可以更方便地對整個跳躍表進行處理,比如快速訪問跳躍表的表頭節點和表尾節點,或者快速地獲取跳躍表節點的數量等信息。

typedef struct zskiplist {
//表頭節點和表尾節點
struct zskiplistNode *header, *tail;
//表中節點的的數量
unsigned long length;
//表中層數最大的節點層數
int level;
} zskiplist;

 

  header和tail指針分別指向跳躍表的表頭和表尾節點,通過這兩個指針,程序定位表頭及誒點和表尾節點的復雜度為O(1)。

       通過使用length屬性來記錄節點的數量,程序可以在O(1)復雜度內返回跳躍表的長度。

       level屬性則用於在O(1)復雜度內獲取跳躍表中層高最大的那個節點的層數量,注意表頭節點的層高並不計算在內。

跳躍表API

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM