1. 程式人生 > 其它 >《Redis核心技術與實戰》學習總結(2)

《Redis核心技術與實戰》學習總結(2)

上一篇總結了一個KV資料庫的基本架構 和 Redis的底層資料結構概覽,重點總結了Sorted Set的兩個資料結構的切換,但沒有介紹List的兩個資料結構的切換,因此本文試著總結一下。

1 上一篇的遺留問題

上一篇總結了一個KV資料庫的基本架構 和 Redis的底層資料結構概覽,重點總結了Sorted Set的兩個資料結構的切換,但沒有介紹List的兩個資料結構的切換,因此本文試著總結一下。

這裡先直接給出答案:

從上圖可以看到,當List的資料滿足下面兩個條件時,就會使用壓縮列表,否則使用雙向連結串列。

(1)列表物件儲存的所有字串元素的長度都小於64位元組;

(2)列表物件儲存的元素數量小於512個;

這兩個引數其實也是可以在redis.conf中修改的:

list-max-ziplist-value 64 
list-max-ziplist-entries 512 

2 Redis 3.2之前的實現

由上一篇已經知道,List型別的底層實現包括了雙向連結串列和壓縮列表,但這是在Redis的3.2版本之前的底層實現。而從Redis 3.2版本開始,Redis修改了List的底層實現,將壓縮列表 和 雙向連結串列 結合,我們稱它為 quickList 快速列表。

從第一節的內容我們已經知道,當建立一個新的List時,Redis會優先使用壓縮列表,然後在有需要的時候,再轉成雙向連結串列

Redis為什麼要這麼設計呢?

因為,雙向連結串列的記憶體佔用 比 壓縮列表多,而壓縮列表的設計初衷就在於節約記憶體。眾所周知,Redis之所以快的原因之一就是它是記憶體資料庫

,所有操作都在記憶體上完成,因此對於記憶體的佔用有要求。

雙向連結串列

壓縮列表

畫外音:在Redis 3.2 之前,我們也可以通過命令來驗證:

192.168.80.100:6379> rpush testkey "edison" "andy" "leo"
3
192.168.80.100:6379> object encoding testkey
ziplist
192.168.80.100:6379> rpush testkey "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"
4
192.168.80.100
:6379> object encoding testkey linkedlist

那麼,壓縮列表為什麼佔用記憶體少呢?

其實從上面的圖和下面的原始碼也可以看出來,壓縮列表並沒有維護雙向指標prev 和 next,而只是儲存了上一個entry的長度 和 下一個entry的長度,通過長度來推算下一個entry在哪裡。

typedef struct zlentry {    // 壓縮列表節點
    unsigned int prevrawlensize, prevrawlen;    // prevrawlen是前一個節點的長度,prevrawlensize是指prevrawlen的大小,有1位元組和5位元組兩種
    unsigned int lensize, len;  // len為當前節點長度 lensize為編碼len所需的位元組大小
    unsigned int headersize;    // 當前節點的header大小
    unsigned char encoding; // 節點的編碼方式
    unsigned char *p;   // 指向節點的指標
} zlentry;

這是一種典型的“時間換空間”的方法,即犧牲讀取的效能,換取極致的儲存空間。由於壓縮列表儲存在一段連續的記憶體上,所以它的儲存效率還是蠻高的。

但是,此種設計只適合在欄位個數、值比較小的時候,一旦長度過長,壓縮列表的設計(利於讀取但不利於修改的初衷)會導致修改和刪除操作需要頻繁的申請和釋放記憶體,可能會導致大量的資料拷貝,拖慢Redis的整體效能

因此,Redis選擇了在達到閾值時,切換資料結構為雙向連結串列。

3 Redis 3.2之後的實現

在Redis 3.2及之後,Redis選擇了結合壓縮列表 和 雙向連結串列的優點,形成了一個新的底層實現:quicklist 快速列表。

快速列表是一個壓縮列表組成的雙向連結串列,每個節點使用壓縮列表來儲存資料。換句話說,快速列表中儲存了一個個小的壓縮列表。其結構如下圖所示:

為了進一步節約空間,Redis 還會對壓縮列表進行壓縮儲存(一種無失真壓縮演算法LZF),這取決壓縮深度的引數設定,我們可以選擇不壓縮(預設值不壓縮)也可以 選擇壓縮中間節點。

畫外音:兩端節點一般不被壓縮,因為當一個連結串列很長時,最頻繁訪問的就是兩端的資料,根據“二八定律”,兩端資料不壓縮,而將中間資料壓縮,從而節省空間,但又保證讀取效率。

此外,對於每個壓縮列表的大小,也是可以通過在redis.conf中的引數來設定的:

list-max-ziplist-size -2

引數可選值從-1到-5,其含義如下:

1) -5:每個quicklist節點上的ziplist大小不能超過64kb。

2) -4:每個quicklsit節點上的ziplist大小不能超過32kb。

3) -3:每個quicklsit節點上的ziplist大小不能超過16kb。

4) -2:每個quicklsit節點上的ziplist大小不能超過8kb。

5) -1:每個quicklsit節點上的ziplist大小不能超過4kb。

畫外音:在Redis 3.2 之後,我們也可以通過命令來驗證:

192.168.80.100:6379> rpush testkey "edison" "andy" "leo"
3
192.168.80.100:6379> object encoding testkey
quicklist
192.168.80.100:6379> rpush testkey "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"
4
192.168.80.100:6379> object encoding testkey
quicklist

綜述,快速列表的本質其實是對壓縮列表的一次封裝,使用小塊的壓縮列表來組織,既可以保證記憶體佔用較小,也可以保證操作效能

4 總結

本文總結了Redis的List型別在何時使用壓縮列表,何時使用雙向連結串列,以及快速列表的基本概念。當然,更多的內容還是需要自行去搜索學習,意猶未盡的童鞋也可以去分析原始碼。最後,如果你對其他集合型別也有此類問題,你可以參考下面附錄中的內容,而至於Why,則可以自行百度搜索瞭解。

Anyway,對於Redis集合型別的底層思想採用了兩種資料結構的設計思想是值得我們學習借鑑的,它其實充分體現了軟體設計中的Tradeoff(權衡)思想。對於Redis來說,即在主體目標是保證效能的大約束前提下,權衡多方因素如操作時間和空間佔用,以達到較為穩定的執行表現。對於軟體設計來說,也需要在時間 vs 空間,新技術 vs 老技術,優雅 vs 效率,輕度設計 vs 重度設計等之間做權衡,一個問題總會有多種解決方案可以實現,在特定的時間段,永遠沒有最完美的設計,只有較合適的設計。在實際中,它可能結合了多種因素的考慮,不斷地去粗取精,迭代為更好的設計。

附錄

Hash:

Set:

Sorted Set(zset):

參考資料

極客時間,蔣德鈞《Redis核心技術與實戰》

作者:周旭龍

出處:https://edisonchou.cnblogs.com

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。