Redis基礎知識(學習筆記12--集合的底層實現原理)

东山絮柳仔發表於2024-07-02

1. 兩種實現的選擇

對於Hash 與 Zset 集合,其底層的實現有兩種:壓縮表ziplist 和 跳躍表skiplist。著兩種實現對於使用者來說是透明的,但使用者寫入不同的資料,系統會自動使用不同的實現。只有同時滿足以配置檔案redis.conf中相關集合元素數量閾值與元素大小閾值兩個條件,使用的就是壓縮表,只要有一個條件不滿足使用的就是跳躍表skiplist.例如,對於ZSet集合中這兩個條件如下:

***集合元素個數小於redis.conf中zset-max-ziplist-entries屬性的值,其預設值為128。

***每個集合元素大小都小於redis.conf中zset-max-ziplist-value屬性的值,其預設值為64位元組。

從配置檔案中檢視,當然也可以透過命令檢視,例如:

檢視zset的

> config get zset-*-ziplist-*

檢視hash的

> config get hash-*_ziplist-*

2. ziplist

(1) 什麼是ziplist

ziplist,通常稱為壓縮列表,是一個經過特殊編碼的用於儲存字串或整數的雙向連結串列。其底層資料結構由三部分組成:head、entries 與 end,這三部分在記憶體上是連續存放的。

(2)head

head又由三部分構成:

***zlbytes:佔4個位元組,用於存放ziplist列表整體資料結構所佔的位元組數,包括zlbytes本身的長度。

***zltail:佔4個位元組,用於存放ziplist中最後一個entry在整個資料結構中的偏移量(位元組)。該資料的存在可以快速定位列表的尾entry位置,以方便操作。

***zllen:佔有兩個位元組,用於存放列表包含entry個數。由於其只有16位,所以ziplist最多可以含有的entry個數為2^16-1 =65535個。

(3) entries

entries是真正的列表,由很多的列表元素entry構成。由於不同的元素型別、數值的不同,從而導致每個entry的長度不同。

每個entry由三部分構成:

*** prvelength:該部分用於記錄上一個entry的長度,以實現逆序遍歷,預設長度為1位元組,只要上一個entry的長度<254位元組,prvelength就佔1位元組,否則其會自動擴充套件為3位元組長度。

【為什麼是254呢?因為255 是全是1,是end的識別符號,需要預留出來;254 是自動擴充套件的識別符號,也是預留的。】

*** encoding:該部分用於標誌後面data的具體型別。如果data為整數型別,encoding固定長度為1位元組。如果data為字串型別,則encoding長度可能會是1位元組、2位元組或5位元組。data字串不同的長度,對應不同的encoding長度。

*** data:真正儲存的資料。資料型別只能是整數型別或字串型別。不同的資料佔用的位元組長度不同。

(4)end

end 只包含一部分,稱為zlend,佔1個位元組,值固定為255,即二進位制全為1,表示一個ziplist列表的結束。

總結

3.listPack

對應ziplist,實現複雜,未來逆序遍歷,每個entry中包含一個entry的長度,這樣會導致在ziplist中修改或者插入時需要進行級聯更新。在高併發的寫常見下會極度降低redis的效能。為了實現更緊湊、更快的解析,更簡單的實現,重寫實現了ziplist,並命名為listPack。

在redis 7.0 中,已經將ziplist全部替換為listPack,但為了相容性,在配置中也保留了ziplist的相關屬性。

>config get zset-max-listpack-*

注意:hash一樣。

(1) 什麼是listPack

listPack也是一個經過特殊編碼的用於儲存字串或整數的雙向連結串列。其底層資料結構也由三部分組成:head、entries 與 end,並且這三部分在記憶體上也是連續存放的。

listPack 與 ziplist的重大區別在head 與每個entry的結構上,表示列表結束的end與ziplist的zlend是相同的,佔一個位元組,且8位全為1。

(2)head

head由兩部分組成:

***totalBytes:佔4個位元組,用於存放listPack列表整體資料結構所佔的位元組數,包括totalBytes本身的長度。

***elemNum:佔2個位元組,用於存放列表包含的entry個數。其意義與zipList中zllen的相同。

與ziplist的head相比,沒有了記錄最後一個entry偏移量的zltail。

(3)entries

entries 也是listPack中真正的列表,由很多的列表元素entry構成。由於不同的元素型別、數值的不同,從而導致每個entry的長度不同,但與ziplist的entry結構相比,listPack的entry結構發生了較大變化。

其中最大的變化就是沒有了記錄前一個entry長度的prvelength,而增加了記錄當前entry長度的element-total-len。而這個改變仍然可以實現逆序遍歷,但卻避免了由於在列表中間修改或插入entry時引發的級聯更新。

每個entry仍由三部分構成:

***encoding:該部分用於標誌後面的data的具體型別。如果data為整數型別,encoding長度可能會是1、2、3、4、5或9位元組。不同的位元組長度,其標識位不同。如果data為字串型別,則encoding長度可能會是1、2或5位元組。data字串不同的長度,對應著不同的encodingc長度。

*** data:正常儲存的資料。資料型別只能是整數型別或字串型別。不同的資料佔用的位元組長度不通。

***element-total-len:該部分用於記錄當前entry的長度,用於實現逆序遍歷。由於其特殊的記錄方式,使其本身佔有的字元資料可能會是1、2、3、4 或5位元組。

(4)總結

4.skiplist

(1) 什麼是skiplist

skiplist,跳躍列表,簡稱跳錶,是一種隨機化的資料結構,基於並聯的連結串列,實現簡單,查詢效率較高。簡單來說跳錶也是連結串列的一種,只不過它在連結串列的基礎上增加了跳躍功能。也正是這個跳躍功能,使得在查詢元素時,能夠提供較高的效率。

(2)skipList原理

假設有一個帶頭尾節點的有序列表。

在該連結串列中,如果要查詢某個資料,需要從頭到尾逐個進行比較,直到找到包含資料的那個節點,或者找到第一個比給定資料大的節點,或者找到最後尾節點,後兩種都屬於沒有找到的情況。同樣,當我們需要新插入資料 時候,也要經歷同樣的查詢過程,從而確定插入位置。

為了提升查詢效率,在偶數節點上增加一個指標,讓其指向下一個偶數節點。

這樣所有偶數節點就連成了一個新的連結串列(簡稱高層連結串列),當然,高層連結串列包含的節點個數只是原來連結串列的一半。此時再想查詢某個資料時,先沿著高層連結串列進行查詢。當遇到第一個比待查資料大的節點時,立即從該大節點的前一個節點回到原連結串列中繼續進行查詢。例如,若想插入一個資料20,則先在(8,19,31,42)的高層連結串列中查詢,找到第一個比20大的節點31,然後再在高層連結串列中找到31節點的前一個節點19,然後再在原連結串列中獲取到下一個節點值為23。比20大,則20插入到19節點與23節點之間。若插入的是25,比節點23大,則插入到23節點與31節點之間。

該方式明顯可以減少比較次數,提高查詢效率。如果連結串列元素較多,為了進一步提升查詢效率,可以將原連結串列構建為三次連結串列(3的倍數為3級),或更高層級連結串列。

(3)存在問題

這種對連結串列分層級的方式從原理看確實提升了查詢效率,但再實際操作時,就出現了問題:由於固定序號的元素擁有固定層級,所以列表元素出現增加或者刪除的情況下,會導致列表整體元素層級大調整,但這樣勢必會大大降低系統效能。並且元素節點越靠前,變動的代價越大。

(4)演算法最佳化

解決方案,每一個節點,它的層級是隨機的分配的,這樣增加或刪除節點,對其它不相鄰的節點沒有影響。

(5)知識點補充

跳錶的結構體原始碼

*** ele,用於儲存該節點的元素

*** score,用於儲存節點的分數(節點按照 score 值排序,score 值一樣則按照 ele 排序)

*** *backward,指向上一個節點

*** level[] 是實現跳錶多層次指標的關鍵所在,level 陣列中的每一個元素代表跳錶的一層,比如 leve[0] 就表示第一層,leve[1] 就表示第二層。

zskiplist Level 結構體裡定義了指向下一個跳錶節點的指標** *forward 和用來記錄兩個節點之間的距離 span

5. quickList

(1) 什麼是quickList

quickList,快速列表,quickList本身是一個雙向無迴圈連結串列,它的每一個節點都是一個zipList,從redis 3.2 版本開始,對於List的底層實現,使用quickList代替了zipList 和 linkedList。

zipList 和 linkedList 都存在有明顯不足,而quickList則對它們都進行了改進:吸收了zipList 和 linkedList的優點,避開了它們的不足。

quickList 本質上是 zipList 和 linkedList的混合體。其將linkedList按段切分,每一段使用zipList來緊湊儲存若干真正的資料元素,多個zipList之間使用雙向指標串接起來。當然,對於每個zipList中最多可存放多大容量的資料元素,在配置檔案中透過list-max-ziplist-size屬性指定。

【補充:為什麼不用linkedlist? 連結串列的附加空間相對太高,prev 和 next指標就要佔去16個位元組,另外,每個節點的記憶體都是單獨分配,會加劇記憶體的碎片化,影響記憶體的管理效率,所以,後來的新版本的redis對列表資料結構進行了改造,使用uickList 代替了 zipList 和 linkedList。】【ziplist,是連續的,所以插入、刪除的是昂貴的,對記憶體空間需要頻繁的申請和釋放。】

(2)檢索操作

對於list元素的檢索,都是以其索引index為依據的。quickList由一個個的zipList構成,每個zipList的zllen中記錄的就是當前zipList中包含的entry的個數,即包含的真正資料元素的個數。根據要檢索元素的index,從quickList的頭節點開始,逐個對zipList的zllen做sum求和,直到找到第一個求和後sum大於index的zipList,那麼要檢索的這個元素就在這個zipList中,只要遍歷這個zipList中的entries 即可。

(3)插入操作

由於ziplist是有大小限制的,所以在quick中插入一個元素在邏輯上相對比較複雜一些。

(4)刪除操作

刪除相對簡單,檢索,刪除即可。但是需要考慮的是,如果ziplist只有一個元素,這個要想到刪除後,ziplist就不再包含元素了,要進行ziplist的釋放。

6.key 與 value中元素的數量

Redis的各種特殊資料結構的設計,不僅極大提升了Redis的效能,並且還使得Redis可以支援的Key的數量、集合Value中可以支援的元素數量都可以非常龐大。

*** Redis 最多可以處理2^32個key(約42億),並且在實踐中經過測試,每個Redis例項至少可以處理2.5億個key。

*** 每個Hash、List、Set、ZSet集合都可以包含2^32個元素。

學習參閱宣告

【Redis影片從入門到高階】

https://www.bilibili.com/video/BV1U24y1y7jF?p=11&vd_source=0e347fbc6c2b049143afaa5a15abfc1c】

相關文章