《Redis設計與實現》讀書筆記(四) ——Redis中的跳躍表
《Redis設計與實現》讀書筆記(四) ——Redis中的跳躍表
(原創內容,轉載請註明來源,謝謝)
一、概述
跳躍表(skiplist)是一種有序的資料結構,它通過每個節點中維持多個指向其他節點的指標,從而實現快速訪問。跳躍表平均O(logN),最壞O(N),支援順序遍歷查詢。
在redis中,有序集合(sortedset)的其中一種實現方式就是跳躍表。當有序集合的元素較多,或者集合中的元素是比較常的字串,則會使用跳躍表來實現。另外,在redis叢集節點中的內部資料結構,也是用跳躍表實現。
二、跳躍表實現
跳躍表是由各個跳躍表節點組成。
1、跳躍表資料結構
typedef structzskiplist{ struct zskiplistNode *header,*tail; unsigned long length; int level; }zskiplist;
上圖最左邊就是跳躍表的結構。
其中,header和tail是跳躍表節點的頭結點和尾節點,length是跳躍表的長度(即跳躍表節點的數量,不含頭結點),level表示層數中最大節點的層數(不計算表頭結點)。
因此,獲取跳躍表的表頭、表尾、最大層數、長度的時間複雜度都是O(1)。
2、跳躍表節點資料結構
typedef structzskiplistNode{ struct zskiplistLevel{ struct zskiplistNode *forward; unsigned int span; }level[]; struct zskiplistNode *backward; double score; robj *obj; }zskiplistNode;
上圖右邊四列就是跳躍表節點的結構。
跳躍表節點包含下列四個概念:
1)層(level)。
zskiplistNode中的zskiplistLevel就表示層。上圖L1、L2等,也就是層。L1是第一層,L2是第二層,以此類推。
每個層都有兩個屬性,前進指標(forward)和跨度(span)。前進指標用於訪問表尾方向的節點,便於跳躍表正向遍歷節點的時候,查詢下一個節點位置;跨度記錄前進指標所指的節點和當前節點的距離,用於計算排位,訪問過程中,將沿途訪問的所有層的跨度累計起來,得到的結果就是跳躍表的排位。上圖中帶數字的箭頭就表示前進指標,箭頭上面的數字就是跨度。當程式從表頭向表尾遍歷,會沿著前進指標進行。
層的意義:
層的作用是加快訪問速度,這也是跳躍表的核心思想。每次建立跳躍表後,根據冪次定律,越大的數字出現的概率越小,隨機生成一個1~32之間的值作為level陣列的大小,這個就是層的高度。
越高層出現的概率越低,這也是實現跳躍表的中心思想。類似的概念如圖所示:
每個節點的層都是隨機的,但是每個節點高層的概率都是,越高層概率越低。通過這個高層,可以快速查詢到score是某個值(或某個範圍的值)的跳躍表節點。
查詢方式是,先從最高層進行查詢,由於其從小到大排序,因此只要在最高層中確定其在哪兩個節點對應的範圍之間。其次,再從次高層查詢。以此類推,直到在第一層的從小到大遍歷,可以確定節點在或不在此跳躍表中。
下圖分別是帶有1、3、5個層的跳躍表節點:
程式遍歷節點的時候,會從頭結點開始,沿著zskiplistLevel[i].span=1,從高層開始,逐個遍歷下一個節點。直到發現zskiplistLevel[i].forward=null,表示已經完成全部的遍歷。
2)後退指標(backward)。
zskiplistNode中的backward就表示後退指標。在上圖的節點中用BW來表示,其指向當前節點的前一個節點,用於反向遍歷時候使用。
不過後退指標每次只能往回退一個節點,不像前進指標可以一次跳過很多個節點。並且,後退指標不會指向頭節點。
3)分值(score)。
zskiplistNode中的score就表示分值。各節點中的數字就是分值,跳躍表中,節點按照分值從小到大排列。
這個分值,即儲存有序集合中的score。
4)成員物件(obj)。
zskiplistNode中的obj就表示成員物件的指標,其指向儲存著節點的具體內容的redis的sds型別的字串。
這個obj,即指向有序集合對應的具體值。因此,每個節點的成員物件唯一。
但是,每個節點的分值可以一樣。分值一樣時,會按照成員變數在字典中的大小順序,小的靠近表頭。
——written by linhxx 2017.08.30