【最完整系列】Redis-結構篇-快速列表

注意:本系列文章分析的 Redis 源碼版本:github.com/Sidfate/red…,是文章發佈時間的最新版。node

前景提要

在介紹快速列表以前,建議你要先了解下 ziplist 和 adlist,特別是 ziplist (參考個人文章《【最完整系列】Redis-結構篇-壓縮列表》),關於 adlist 下面我會簡單解釋一下。git

adlist

adlist 其實就是一個常規的雙向鏈表實現,show you source code:github

typedef struct listNode {
        struct listNode *prev;
        struct listNode *next;
        void *value;
    } listNode;
    
    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

quicklist

ok,在咱們切入正題前,先講下在早期的 redis 版本中,列表鍵的實現分 2 種,元素數量少時用 ziplist,多時用 adlist,但在 3.2(網上資料查到的,不必定準確)以後的版本里,都用 quicklist 取代:算法

> lpush test_list 1
    (integer) 1
    > object encoding test_list
    "quicklist"
複製代碼

爲何要專門設計一個 quicklist 來從新定義呢?接下來從源碼的角度來解讀。首先仍是看註釋(redis 源碼的註釋真香):shell

quicklist.c - A doubly linked list of ziplists數據結構

而後再看下源碼結構實現:post

typedef struct quicklistNode {
        struct quicklistNode *prev;
        struct quicklistNode *next;
        unsigned char *zl;         
        unsigned int sz;             /* ziplist 佔用的字節總數 */
        unsigned int count : 16;     /* ziplist 的元素個數 */
        unsigned int encoding : 2;   /* 是否被壓縮,2表示被壓縮,1表示原生 */
        unsigned int container : 2;  
        unsigned int recompress : 1; 
        unsigned int attempted_compress : 1; 
        unsigned int extra : 10; 
    } quicklistNode;
    
    typedef struct quicklist {
        quicklistNode *head;
        quicklistNode *tail;
        unsigned long count;        /* 全部 ziplists 的元素總和 */
        unsigned long len;          /* quicklistNodes 的個數 */
        int fill : 16;              
        unsigned int compress : 16; 
    } quicklist;
複製代碼

字段的詳細解釋請參照本文末尾的字段詳解部分。性能

所你們明白了嗎,爲何我在一開始讓你們先去了解下 ziplist 和 adlist,由於 quicklist 其實就是一個以 ziplist 爲節點(quicklistNode 中存放指向 ziplist 的指針)的 adlist,沒圖說個JB:單元測試

知道結構後再回到咱們最初的問題,quicklist 的結構爲何這樣設計呢?quicklist 平衡了 ziplist 和 adlist 的優缺點:

  • 雙向鏈表便於在表的兩端進行 push 和 pop 操做,可是它的內存開銷比較大。首先,它在每一個節點上除了要保存數據以外,還要額外保存兩個指針;其次,雙向鏈表的各個節點是單獨的內存塊,地址不連續,節點多了容易產生內存碎片,不利於內存管理。
  • ziplist 因爲是一整塊連續內存,因此存儲效率很高。可是,它不利於修改操做,每次數據變更都會引起一次內存的 realloc。特別是當 ziplist 長度很長的時候,一次 realloc 可能會致使大批量的數據拷貝,進一步下降性能。

可是問題仍是類了,quicklist 中 quicklistNode 包含多長的 ziplist 多少合適呢?長度若是小了,跟普通的雙向鏈表也就差很少了,仍是有內存碎片的問題;長度大了,每一個 quicklist 節點上的 ziplist 須要大片的連續內存,操做內存的效率仍是降低了。因此這個長度確定是一個平衡值,它是 redis 提供的一個選項配置,默認是 -2,來看下官方說明:

# -5: max size: 64 Kb  <-- not recommended for normal workloads
# -4: max size: 32 Kb  <-- not recommended
# -3: max size: 16 Kb  <-- probably not recommended
# -2: max size: 8 Kb   <-- good
# -1: max size: 4 Kb   <-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
list-max-ziplist-size -2
複製代碼

這裏我就不翻譯了,原生英文解釋的很清楚了,惟一一點要說明下的是,當 list-max-ziplist-size 設置爲正數時,表示每一個 list 節點中儲存元素個數。

壓縮機制

你們仔細看 quicklist 的源碼結構時,可能還注意到出現了不少 compress 的字樣,這是由於 redis 爲 quicklist 提供了一套壓縮機制。

當 quicklist 很長的時候,最容易被訪問的極可能是兩端的數據,中間的數據被訪問的頻率比較低(訪問起來性能也很低)。若是應用場景符合這個特色,redis 還提供了一個選項,可以把中間的數據節點進行壓縮,從而進一步節省內存空間。Redis的配置參數 list-compress-depth 就是用來完成這個設置的。

# 列表也能夠被壓縮。
# 壓縮深度指的是列表兩側開始不須要 ziplist 節點的深度(下面會解釋)。
#	爲了執行快速的 push/pop 操做,列表的頭和尾一般不壓縮。
# 設置以下:
# 0: 禁用壓縮機制
# 1: 壓縮深度 1 表示壓縮除了頭和尾以外的全部內部節點。例如結構:
#    [head]->node->node->...->node->[tail]
#    由於[head], [tail]永遠不會被壓縮,它們直接的 node 都後被壓縮。
# 2: [head]->[next]->node->node->...->node->[prev]->[tail]
#    2 表示不壓縮 head,head->next,tail->prev 和 tail, 它們以前的 node 都壓縮。
# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
# 以此類推...
list-compress-depth 0
複製代碼

這個參數默認是 0 也就是不壓縮,Redis對於 quicklist 內部節點的壓縮算法,採用的 LZF —— 一種無損壓縮算法,有興趣的能夠看下 zh.wikipedia.org/wiki/LZFSE

結構字段詳解

以前的內容已經對源碼結構中大多數的字段作了說明,可是還遺留一些字段我在這裏統一解釋下。

首先補充一個結構 quicklistLZF,後面的說明中會出現:

typedef struct quicklistLZF {
        unsigned int sz; /* LZF size in bytes*/
        char compressed[];
    } quicklistLZF;
複製代碼
屬性 大小 含義
prev 8字節 指向鏈表前一個節點的指針。
next 8字節 指向鏈表後一個節點的指針。
zl 8字節 數據指針。若是當前節點的數據沒有壓縮,那麼它指向一個 ziplist 結構;不然,它指向一個 quicklistLZF 結構。
sz 4字節 ziplist 佔用的字節總數。若是指向的 ziplist 被壓縮,仍然表示壓縮前的字節總數。
count 16位 ziplist 包含的元素個數。
encoding 2位 表示ziplist是否壓縮了。目前只有兩種取值:2表示被壓縮了(並且用的是LZF壓縮算法),1表示沒有壓縮。
container 2位 數據容器。爲1時表示 NONE,即一個 quicklist 節點下面直接存數據,爲2時表示ZIPLIST,即便用ziplist存數據。
recompress 1位 bool值,當咱們使用相似lindex這樣的命令查看了某一項原本壓縮的數據時,須要把數據暫時解壓,這時就設置 recompress = 1 作一個標記,等有機會再把數據從新壓縮。
attempted_compress 1位 bool值,在單元測試的時候用到。
extra 10位 擴展字段,以備後用。

須要注意的是,我發現網上的不少文章都沒有提到的一點,官方給出的解釋中說明了 quicklistNode 是一個 32 字節的結構,這應該是針對 64 位系統而言的,由於 prev,next 和 zl 都是指針,在 64 位系統中佔 8 字節,如下的結構同理。

屬性 大小 含義
head 8字節 指向頭節點的指針。
tail 8字節 指向尾節點的指針。
count 8字節 全部 ziplists 的元素總個數。
len 8字節 quicklist 節點的個數。
fill 16位 ziplist大小設置,存放 list-max-ziplist-size 參數的值。
compress 16位 節點壓縮深度,存放 list-compress-depth 參數的值。

另外須要注意一點的是 quicklist 結構在 64 位系統中是佔 40 個字節,可是如上計算我得出的長度是 36 字節,這裏面涉及到告終構體字節對齊約定,目的的話仍是爲了提高數據的讀取效率。

屬性 大小 含義
sz 4字節 壓縮後的ziplist大小
compressed 待定 LZF 壓縮後的數據

爲何 quicklistNode 中的 count 用 16 位就能夠表示?

咱們已經知道,ziplist 大小受到 list-max-ziplist-size 參數的限制。按照正值和負值有兩種狀況:

  • 當這個參數取正值的時候,就是剛好表示一個 quicklistNode 結構中 zl 所指向的 ziplist 所包含的數據項的最大值。list-max-ziplist-size 參數是由quicklist結構的 fill 字段來存儲的,而 fill 字段是 16bit,因此它所能表達的值可以用 16bit 來表示。
  • 當這個參數取負值的時候,可以表示的 ziplist 最大長度是 64 Kb。而 ziplist 中每個數據項,最少須要 2 個字節來表示(詳見《【最完整系列】Redis-結構篇-壓縮列表》)。因此,ziplist中數據項的個數不會超過 32 Kb,用 16bit 來表達足夠了。
相關文章
相關標籤/搜索