《Redis設計與實現》學習筆記(未完--持續更新) 三、字典(map) 四、跳躍表(skiplist) 五、整數集合(intset)

一、字符串 SDS

Redis的底層的字符串並不是使用C語言字符串(C字符串),而是自己定義了動態字符串
五種數據類型對應的實現:String
記錄長度

C字符串由於沒有記錄字符串長度,每次執行計算長度時都會每個字符進行計數,時間複雜度是O(N);在SDS由於記錄了必要的空間長度,所以redis就算反覆執行計算字符串長度時時間複雜度都是O(1)

防止緩存溢出

在C字符串中由於不記錄自己的字符串長度,如果在執行修改字符串時沒有提前分配空間就會造成長度溢出,而SDS記錄了必要的空間長度,所以每次進行字符串修改操作時,會檢查自身的空間是否足夠容納字符串的需求,檢查如果空間不足時就會先進行內存重新分配纔會執行操作,由於記錄了必要的空間長度,所以這個檢查的性能消耗是幾乎可以忽略的。

減少內存重新分配

SDS通過減少了空間的重新分配,所以有效的提升了性能,SDS通過以下手段減少空間的重新分配:

  • 1、空間預分配
    在修改字符串時,SDS檢查空間不足進行空間拓展時,如果拓展的空間小於1MB,就會拓展到字符串長度的兩倍,例如一個字符串長度是13字節,SDS就會拓展到13+13+1(1字節是保存'\0',SDS對此不作記錄),這樣SDS就額外多了13字節,當再有修改操作時就會檢查額外空間,記錄在free字段中,當要在進行修改時SDS就會檢查額外空間加現有是否足夠,足夠就不需要進行內存重新分配,如果拓展的空間大於1MB時,就會直接在所需空間的基礎上再多分配1MB的內存空間。

通過空間的預分配策略,可以把修改N次字符串的內存重新分配從C字符串的至少N次,減少到SDS的最多N次
  • 2、惰性空間釋放
    當修改字符串是一個縮短操作時,SDS並不會把空間釋放掉,而是使用free記錄額外空間,以備以後使用,這樣就又減少了內存重新分配的次數
二進制安全

C字符串只能存儲文本數據不能存儲二進制數據,而Redis提供了API處理二進制數據使SDS能夠存儲圖片、視頻、壓縮文件等二進制數據

兼容C字符串函數

SDS有自己的API也能使用C字符串的函數

總結:

二、雙向鏈表(list)

Redis使用的C語言中並沒有鏈表結構,Redis構建了自己的雙向鏈表作爲list的數據結構之一,因爲雙向鏈表佔用的內存比壓縮列表要多, 所以當創建新的列表鍵時, 列表會優先考慮使用壓縮列表, 當list的元素比較多或者元素的長度都比較長的時候, 才從壓縮列表實現轉換到雙向鏈表。

五種數據類型對應的實現:List

Redis中定義的雙向鏈表listNode:

listNode組成雙向鏈表,list用來持有和操作鏈表,結構如下:

三、字典(map)

C語言中沒有內置字典的數據結構,Redis構建了自己的字典結構,Redis中的字典採用哈希表作爲底層實現,一個哈希表有多個節點,每個節點保存一個鍵值對。Redis的數據庫底層就是字典

字典是Hash的底層實現的數據結構之一,Hash默認使用壓縮列表,當Hash包含的鍵值對比較多,或者鍵值對中的元素都是比較長的字符串時,Redis就會使用字典作爲Hash的底層實現。

hash使用字典的條件:

  • hash對象保存的鍵和值字符串長度都大於64字節
  • hash對象保存的鍵值對數量大於512
五種數據類型對應的實現:Hash

下面是redis字典的哈希表結構定義如下:

hash表又是由dictEntry組成的,table中每個元素都會指向一個dictEntry

下面是dictEntry的結構定義,每個dictEntry就是一個鍵值對,結構定義如下:

字典是由哈希表組成的,哈希表由dictEntry組成,下面是字典的結構定義:

,其中type保存了一組操作鍵值對的函數,privdata是保存了傳給函數的可選參數,ht數組中ht[0]是哈希表,h[1]是用於擴容時使用的。

下面是字典、哈希表、dictEntry的組成結構:,hash表的算法是使用sizemask計算出索引值,從而決定放在哈希數組table的哪個位置如下:其中使用鏈地址法解決哈希衝突

rehash的步驟:
哈希表擴展的條件

在bgsave或者bgwriteaof是數據異步備份複製的操作,異步操作會創建子進程來完成,子進程會按照寫時複製來優化,所以爲了避免不必要的內存寫入,最大限度的節約內存,在子進程運行期間儘量減少哈希表擴展操作

當哈希表的加載因子小於0.1時會自動進入縮容操作

漸進式rehash

擴容或者縮容是將h[0]鍵值rehash轉移到h[1],這個操作不是一次性的轉移,而是漸進式的

h[0]往h[1]完成完成轉移後,h[0]的空間將會被清理同時被設置爲h[0],h[1]被設置爲h[0],下次rehash時再進行類似的轉移和身份交替
鏈地址法解決哈希衝突

哈希衝突時形成一個單向鏈表解決哈希衝突

可以看到,Redis解決哈希衝突是使用next連接相同鍵值形成一個鏈表,也就是類似java中hashmap的鏈地址法

總結:

四、跳躍表(skiplist)

跳躍表是有序的數據結構,平均複雜度是O(logN),最壞複雜度是O(N),跳躍表是sort set數據類型的底層數據結構實現之一

zskiplistNode是跳躍表的節點結構。zskiplist是保存跳躍表節點信息,比如節點數量,指向頭尾節點的指針等信息。

五種數據類型對應的實現:SortSet

zskiplistNode的結構定義:

zskiplist的結構定義

跳躍表的結構圖及屬性如下:

跳錶插入數據會在原有的數據上加上若干層,指向當前層的下一個節點,節點的層數是隨機的生成的範圍在1到32,生成原理是次冪定律(越大的數隨機生成的機率越小),層數越高查詢其他結點的速度就越快,而插入的順序是按照分值進行從小到大插入,分值相等則按照存儲對象的大小排序也從小到大排,每個結點存儲的對象是唯一的

鏈表的檢索效率非常低,而跳錶改善解決了鏈表的檢索效率低的問題

下面是查詢23的例子,從頭結點開始找,先跳到7,然後跳到19,然後到22,再到23,中間跳過了3和11

總結:

五、整數集合(intset)

整數集合(intset)是實現set數據類型的底層數據結構之一,其底層有兩種實現方式,當value是整數值時,且數據量不大時使用inset來存儲,其他情況都是用字典dict來存儲。整數集合可以保證不會出現重複數據。
contents數組是整數集合的底層實現,整數集合的每一個元素都是contents數組的一個數組項,各個項按照數值的從小到大排序,並且不包含重複項

五種數據類型對應的實現:Set

整數集合數據結構定義如下:

encoding編碼方式:共有三種,INTSET_ENC_INT16、INSET_ENC_INT32和INSET_ENC_INT64三種,分別對應不同的範圍。Redis爲了儘可能地節省內存,會根據插入數據的大小選擇不一樣的類型來進行存儲。默認是INTSET_ENC_INT16

intset的結構圖如下:
升級
降級

不支持降級,一旦升級就會保持升級後的狀態或許再次升級的升級的狀態

總結

六、壓縮列表(ziplist)

壓縮列表是Redis爲了節約內存而開發的,是list和hash的底層實現之一。一個壓縮列表可以包含任意多個節點,每個節點可以保存一個字節數組或者一個整數值。

五種數據類型對應的實現:List,Hash
  • List
    當一個list只包含少量列表項,並且每個列表項要麼就是小整數,要麼就是長度比較短的字符串,redis就會使用壓縮列表來做列表鍵的底層實現
  • Hash
    當一個hash只包含少量鍵值對,並且每個鍵值對的鍵和值要麼就是小整數值,要麼就是長度比較短的字符串,那麼Redis就會使用壓縮列表來做hash的底層實現。

下面是壓縮列表的組成部分:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章