redis 中的跳錶與有序集合--redis 有序集合的實現

看了很多跳錶的文章包括《redis設計與實現》,都沒能很好地瞭解跳錶。

感謝https://www.jianshu.com/p/61f8cad04177  此文。

 

有序集合的實現

有序集合  的實現採用了兩種方式:

當有序集合對象同時滿足以下兩個條件時,對象使用 ziplist 編碼:

1、保存的元素數量小於128;

2、保存的所有元素長度都小於64字節。

否則使用跳錶(skiplist)

 

1. 壓縮列表  ziplist

 

首先需要明確,壓縮列表的產生是Redis爲了節約內存開發的,是一個由一系列特殊編碼的連續內存塊組成的順序性數據結構。一個壓縮列表可以包含任意數量個節點,每個節點可以保存自己數組或者一個整數值。如下圖所示

壓縮列表的結構.png

 

zlbytes記錄整個壓縮列表佔用的內存字節數,在對壓縮列表進行內存重分配或計算zlend的位置時使用。zltail記錄壓縮列表尾節點距離壓縮列表的起始地址有多少字節,通過這個偏移量,可以直接確定尾節點的位置。zllen記錄壓縮列表包含的節點數量,entryX表示各種節點,數量和長度不一定。zlend用於標記壓縮列表的末端。
如圖,如果有一個指針p指向該壓縮列表,則尾巴節點的長度就是指針加上偏移量179(十六進制0xb3=16*11+3=179),列表的長度zllen爲5,表示壓縮列表包含5個節點。zlbytes爲0xd2表示壓縮列表的總長爲210字節。

壓縮列表的計算.png

 

由上可知,每個壓縮列表的節點可以保存一個字節數組或者一個整數值,那麼每個節點肯定也有自己的結構。

1.2 壓縮列表的節點

如圖所示,每個壓縮列表的節點都是由previous_entry_length、encoding、content組成的。下面分別來說一說這三個字段的含義。

節點的字段.png

 

1.2.1 previous_entry_length

previous_entry_length以自己爲單位,記錄的是壓縮列表中前一個節點的長度,previous_entry_length自身的空間長度可以是1字節或者5字節。如果前一個字節的長度小於254自己,就是1字節(前一個節點的長度就保存在這裏面,這兩個值一個是本節點裏這個字段本身的空間大小,存儲的是前一個節點的空間大小,不要弄混了哈)。如果前一個大於254那麼這個字段的空間長度就爲5字節,存儲的值爲大於254的那個值(就是前一個節點的長度)。其中這5個字節,第一個字節會被設置爲0xFF也就是254,之後的四個字節用來保存前一個節點的長度。因爲前一個節點的長度被previous_entry_length屬性記錄了,所以程序可以通過指針的運算根據當前節點的起始地址來計算出前一個節點的起始地址。而壓縮列表的從表尾向表頭的遍歷操作就是通過這個原理實現的,只要我們擁有了一個指向某個節點的起始地址指針,通過這個指針和這個字段,我們可以往回遍歷出所有的節點,最終到達表頭。如下:

壓縮列表的後序遍歷.png

 

1.2.2 encoding

節點encoding屬性記錄了節點的content屬性所保存的數據類型及長度。可以爲一字節、兩字節或者五字節長,值的最高位爲00、01或者10的是字節數組編碼,這種編碼表示節點的content屬性保存着字節數組,數組的長度由編碼除去最高2位之後的其他位記錄。也就是說高2位其實代表的是類型是字節數組還是整數編碼。值的最高位以11開頭的是整數編碼:這種編碼表示節點的content屬性保存着整數值。整數值的類型和長度由編碼除去最高2位之後的其他位的記錄。

1.2.3 content

節點的content屬性負責保存節點的值,節點值可以是一個字節數組或者整數,值的類型和長度由節點的encoding屬性決定。

1.3 連鎖更新

之前說過,每個節點的previous_entry_length都記錄了前一個節點的長度,如果長度小於254那麼previous_entry_length需要用1字節來保存這個長度值。現在假設這種情況:壓縮列表有多個連續的長度介於250-253之間的節點e1-eN。因爲每個節點的長度都小於254字節,所以這些節點的previous_entry_length屬性都是1字節長度。此時如果將一個長度大於254的新節點設置爲壓縮列表的頭節點,那麼這個新節點成爲頭節點,也就是e1節點的前置節點。此時將e1的previous_entry_length擴展爲5字節長度,此時e1又超過了254,於是e2的previous_entry_length也超過了254··· .此時這些節點就會連鎖式的更新,並重新分配空間。除了新增加的節點會引發連鎖更新之外,刪除也會。假設中間有一個小於250的刪除了,也會連鎖更新。同上面所說的類似。因爲連鎖更新在最壞的情況下需要對壓縮列表執行N次空間重分配操作,而每次空間重分配的最壞複雜度爲O(N),所以連鎖更新的最壞複雜度爲`O(N^2)。雖然這很耗費時間,但是實際情況下這種發生的概率非常低的。對很少一部分節點進行連鎖更新絕對不會影響性能的。

 

 

 

2.跳躍表

什麼是跳躍表*
跳躍表是一種有序的數據結構,它通過在每個節點中維持多個指向其他的幾點指針,從而達到快速訪問隊尾目的。跳躍表的效率可以和平衡樹想媲美了,最關鍵是它的實現相對於平衡樹來說,代碼的實現上簡單很多。

跳躍表用在哪
說真的,跳躍表在 Redis 中使用不是特別廣泛,只用在了兩個地方。一是實現有序集合鍵,二是集羣節點中用作內部數據結構

跳躍表原理
我們先來看一下一張完整的跳躍表的圖。(圖片來自《Redis 設計與實現》)

完整跳躍表

 

跳躍表的 level 是如何定義的?
跳躍表 level 層級完全是隨機的。一般來說,層級越多,訪問節點的速度越快

跳躍表的插入
首先我們需要插入幾個數據。鏈表開始時是空的。

鏈表開始

 

插入 level = 3,key = 1
當我們插入 level = 3,key = 1 時,結果如下:

level = 3,key = 1

 

插入 level = 1,key = 2
當繼續插入 level = 1,key = 2 時,結果如下

level = 1,key = 2

 

插入 level = 2,key = 3
當繼續插入 level = 2,key = 3 時,結果如下

level = 2,key = 3

 

插入 level = 3,key = 5
當繼續插入 level = 3,key = 5 時,結果如下

level = 3,key = 5

 

插入 level = 1,key = 66
當繼續插入 level = 1,key = 66 時,結果如下

level = 1,key = 66

 

插入 level = 2,key = 100
當繼續插入 level = 2,key = 100 時,結果如下

level = 2,key = 100

 

上述便是跳躍表插入原理,關鍵點就是層級–使用拋硬幣的方式,感覺還真是挺隨機的。每個層級最末端節點指向都是爲 null,表示該層級到達末尾,可以往下一級跳。

跳躍表的查詢

現在我們要找鍵爲 66 的節點的值。那跳躍表是如何進行查詢的呢?

跳躍表的查詢是從頂層往下找,那麼會先從第頂層開始找,方式就是循環比較,如過頂層節點的下一個節點爲空說明到達末尾,會跳到第二層,繼續遍歷,直到找到對應節點。

如下圖所示紅色框內,我們帶着鍵 66 和 1 比較,發現 66 大於 1。繼續找頂層的下一個節點,發現 66 也是大於五的,繼續遍歷。由於下一節點爲空,則會跳到 level 2。

 

頂層遍歷

上層沒有找到 66,這時跳到 level 2 進行遍歷,但是這裏有一個點需要注意,遍歷鏈表不是又重新遍歷。而是從 5 這個節點繼續往下找下一個節點。如下,我們遍歷了 level 3 後,記錄下當前處在 5 這個節點,那接下來遍歷是 5 往後走,發現 100 大於目標 66,所以還是繼續下沉。

 

第二層遍歷

當到 level 1 時,發現 5 的下一個節點恰恰好是 66 ,就將結果直接返回。

 

遍歷第一層

跳躍表刪除
跳躍表的刪除和查找類似,都是一級一級找到相對應的節點,然後將 next 對象指向下下個節點,完全和鏈表類似。

現在我們來刪除 66 這個節點,查找 66 節點和上述類似。

 

找到 66 節點

接下來是斷掉 5 節點 next 的 66 節點,然後將它指向 100 節點。

 

指向 100 節點

如上就是跳躍表的刪除操作了,和我們平時接觸的鏈表是一致的。當然,跳躍表的修改,也是和刪除查找類似,只不過是將值修改罷了,就不繼續介紹了。

 

 

 



 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章