不是吧?爲了加快速度,Redis竟做了這麼“瘋狂”的設計

前言

列表對象是 Redis 中 5 種基礎數據類型之一,在 Redis 3.2 版本之前,列表對象底層存儲結構有兩種:linkedlist(雙端列表)和 ziplist(壓縮列表),而在 Redis 3.2 版本之後,列表對象底層存儲結構只有一種:quicklist(快速列表),難道通過精心設計的 ziplist 最終被 Redis 拋棄了嗎?

列表對象

同字符串對象一樣,列表對象到底使用哪一種數據結構來進行存儲也是通過編碼來進行區分:

編碼屬性描述object encoding命令返回值OBJ_ENCODING_LINKEDLIST使用 linkedlist 實現列表對象linkedlistOBJ_ENCODING_ZIPLIST使用 ziplist 實現列表對象ziplistOBJ_ENCODING_QUICKLIST使用 quicklist 實現列表對象quicklist

linkedlist

linkedlist 是一個雙向列表,每個節點都會存儲指向上一個節點和指向下一個節點的指針。linkedlist 因爲每個節點之間的空間是不連續的,所以可能會造成過多的內存空間碎片。

  • linkedlist存儲結構

鏈表中每一個節點都是一個 listNode 對象(源碼 adlist.h 內),不過需要注意的是,列表中的 value 其實也是一個字符串對象,其他幾種數據類型其內部最終也是會嵌套字符串對象,字符串對象也是唯一一種會被其他對象引用的基本類型:

typedef struct listNode {
    struct listNode *prev;//前一個節點
    struct listNode *next;//後一個節點
    void *value;//值(字符串對象)
} listNode;

然後會將其再進行封裝成爲一個 list 對象(源碼 adlist.h 內):

typedef struct list {
    listNode *head;//頭節點
    listNode *tail;//尾節點
    void *(*dup)(void *ptr);//節點值複製函數
    void (*free)(void *ptr);//節點值釋放函數
    int (*match)(void *ptr, void *key);//節點值對比函數
    unsigned long len;//節點數量
} list;

Redis 中對 linkedlist 的訪問是以 NULL 值爲終點的,因爲 head 節點的 prev 節點爲 NULL,tail 節點的 next 節點也爲 NULL,所以從頭節點開始遍歷,當發現 tail 爲 NULL 時,則可以認爲已經到了列表末尾。

當我們設置一個列表對象時,在 Redis 3.2 版本之前我們可以得到如下存儲示意圖:

ziplist

壓縮列表在前面已經介紹過,想要詳細瞭解的可以點擊這裏。

linkedlist 和 ziplist 的選擇

在 Redis3.2 之前,linkedlist 和 ziplist 兩種編碼可以進選擇切換,如果需要列表使用 ziplist 編碼進行存儲,則必須滿足以下兩個條件:

  • 列表對象保存的所有字符串元素的長度都小於 64 字節。
  • 列表對象保存的元素數量小於 512 個。

一旦不滿足這兩個條件的任意一個,則會使用 linkedlist 編碼進行存儲。

PS:這兩個條件可以通過參數 list-max-ziplist-value 和 list-max-ziplist-entries 進行修改。

這兩種列表能在特定的場景下發揮各自的作用,應該來說已經能滿足大部分需求了,然後 Redis 並不滿足於此,於是一場改革引發了,quicklist 橫空出世。

quicklist

在 Redis 3.2 版本之後,爲了進一步提升 Redis 的性能,列表對象統一採用 quicklist 來存儲列表對象。quicklist存儲了一個雙向列表,每個列表的節點是一個 ziplist,所以實際上 quicklist 並不是一個新的數據結構,它就是linkedlist 和 ziplist 的結合,然後被命名爲快速列表。

  • quicklist 內部存儲結構

quicklist 中每一個節點都是一個 quicklistNode 對象,其數據結構定義如下:

typedef struct quicklistNode {
    struct quicklistNode *prev;//前一個節點
    struct quicklistNode *next;//後一個節點
    unsigned char *zl;//當前指向的ziplist或者quicklistLZF
    unsigned int sz;//當前ziplist佔用字節
    unsigned int count : 16;//ziplist中存儲的元素個數,16字節(最大65535個)
    unsigned int encoding : 2; //是否採用了LZF壓縮算法壓縮節點 1:RAW 2:LZF
    unsigned int container : 2; //存儲結構,NONE=1, ZIPLIST=2
    unsigned int recompress : 1; //當前ziplist是否需要再次壓縮(如果前面被解壓過則爲true,表示需要再次被壓縮)
    unsigned int attempted_compress : 1;//測試用 
    unsigned int extra : 10; //後期留用
} quicklistNode;

然後各個 quicklistNode 就構成了一個快速列表 quicklist:

typedef struct quicklist {
    quicklistNode *head;//列表頭節點
    quicklistNode *tail;//列表尾節點
    unsigned long count;//ziplist中一共存儲了多少元素,即:每一個quicklistNode內的count相加
    unsigned long len; //雙向鏈表的長度,即quicklistNode的數量
    int fill : 16;//填充因子
    unsigned int compress : 16;//壓縮深度 0-不壓縮
} quicklist;

根據這兩個結構,我們可以得到 Redis 3.2 版本之後的列表對象的一個存儲結構示意圖:

  • quicklist 的 compress 屬性

compress 是用來表示壓縮深度,ziplist 除了內存空間是連續之外,還可以採用特定的 LZF 壓縮算法來將節點進行壓縮存儲,從而更進一步的節省空間,壓縮深度可以通過參數 list-compress-depth 控制:

  • 0:不壓縮(默認值)
  • 1:首尾第1個元素不壓縮
  • 2:首位前2個元素不壓縮
  • 3:首尾前3個元素不壓縮
  • 以此類推

注意:之所以採取這種壓縮兩端節點的方式是因爲很多場景都是兩端的元素訪問率最高的,而中間元素訪問率相對較低,所以在實際使用時,我們可以根據自己的實際情況選擇是否進行壓縮,以及具體的壓縮深度。

  • quicklistNode 的 zl 指針

zl 指針默認指向了 ziplist,上面提到 quicklistNode 中有一個 sz 屬性記錄了當前 ziplist 佔用的字節,不過這僅僅限於當前節點沒有被壓縮(通過LZF 壓縮算法)的情況,如果當前節點被壓縮了,那麼被壓縮節點的 zl 指針會指向另一個對象 quicklistLZF,而不會直接指向 ziplist。quicklistLZF 是一個 4+N 字節的結構:

typedef struct quicklistLZF {
    unsigned int sz;// LZF大小,佔用4字節
    char compressed[];//被壓縮的內容,佔用N字節
} quicklistLZF;
  • quicklist 對比原始兩種編碼的改進

quicklist 同樣採用了 linkedlist 的雙端列表特性,然後 quicklist 中的每個節點又是一個 ziplist,所以quicklist 就是綜合平衡考慮了 linkedlist 容易產生空間碎片的問題和 ziplist 的讀寫性能兩個維度而設計出來的一種數據結構。使用 quicklist 需要注意以下 2 點:

  • 如果 ziplist 中的 entry 個數過少,最極端情況就是隻有 1 個 entry 的壓縮列表,那麼此時 quicklist 就相當於退化成了一個普通的 linkedlist。
  • 如果 ziplist 中的 entry 過多,那麼也會導致一次性需要申請的內存空間過大(ziplist 空間是連續的),而且因爲 ziplist 本身的就是以時間換空間,所以會過多 entry 也會影響到列表對象的讀寫性能。

ziplist 中的 entry 個數可以通過參數 list-max-ziplist-size 來控制:

list-max-ziplist-size 1

注意:這個參數可以配置正數也可以配置負數。正數表示限制每個節點中的 entry 數量,如果是負數則只能爲 -1~-5,其代表的含義如下:

  • -1:每個 ziplist 最多隻能爲 4KB
  • -2:每個 ziplist 最多隻能爲 8KB
  • -3:每個 ziplist 最多隻能爲 16KB
  • -4:每個 ziplist 最多隻能爲 32KB
  • -5:每個 ziplist 最多隻能爲 64KB

列表對象常用操作命令

  • lpush key value1 value2:將一個或者多個 value 插入到列表 key 的頭部,key 不存在則創建 key(value2 在value1 之後)。
  • lpushx key value1 value2:將一個或者多個 value 插入到列表 key 的頭部,key 不存在則不做任何處理(value2 在value1 之後)。
  • lpop key:移除並返回 key 值的列表頭元素。
  • rpush key value1 value2:將一個或者多個 value 插入到列表 key 的尾部,key 不存在則創建 key(value2 在value1 之後)。
  • rpushx key value1 vaue2:將一個或者多個 value 插入到列表 key 的尾部,key 不存在則不做任何處理(value2 在value1 之後)。
  • rpop key:移除並返回列表 key 的尾元素。
  • llen key:返回列表 key 的長度。
  • lindex key index:返回列表 key 中下標爲 index 的元素。index 爲正數(從 0 開始)表示從隊頭開始算,index 爲負數(從-1開始)則表示從隊尾開始算。
  • lrange key start stop:返回列表 key 中下標 [start,end] 之間的元素。
  • lset key index value:將 value 設置到列表 key 中指定 index 位置,key 不存在或者 index 超出範圍則會報錯。
  • ltrim key start end:截取列表中 [start,end] 之間的元素,並替換原列表保存。

瞭解了操作列表對象的常用命令,我們就可以來驗證下前面提到的列表對象的類型和編碼了,在測試之前爲了防止其他 key 值的干擾,我們先執行 flushall 命令清空 Redis 數據庫。

接下來依次輸入命令:

  • lpush name zhangsan
  • type name
  • object encoding name

可以看到,通過 type 命令輸出的是 list,說明當前 name 存的是一個列表對象,並且編碼是 quicklist(示例中用的是 5.0.5 版本)。

總結

本文主要介紹了 Redis 中 5 種常用數據類型中的 列表對象,並介紹了底層的存儲結構 quicklist,並分別對舊版本的兩種底層數據 linkedlist 和 ziplist 進行了分析對比得出了爲什麼 Redis 最終要採用 quicklist 來存儲列表對象。

作者:雙子孤狼
鏈接:https://juejin.cn/post/6918723608055873549
來源:掘金

</article>

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