Redis底層數據結構之list


Redis中另一個常用的數據結構就是list,其底層有linkedListzipListquickList這三種存儲方式。

鏈表linkedList

Java中的LinkedList類似,Redis中的linkedList是一個雙向鏈表,也是由一個個節點組成的。Redis中借助C語言實現的鏈表節點結構如下所示:

//定義鏈表節點的結構體 
typedf struct listNode{
    //前一個節點
    struct listNode *prev;
    //后一個節點
    struct listNode *next;
    //當前節點的值的指針
    void *value;
}listNode;

pre指向前一個節點,next指針指向后一個節點,value保存着當前節點對應的數據對象。listNode的示意圖如下所示:

鏈表的結構如下:

typedf struct list{
    //頭指針
    listNode *head;
    //尾指針
    listNode *tail;
    //節點拷貝函數
    void *(*dup)(void *ptr);
    //釋放節點函數
    void *(*free)(void *ptr);
    //判斷兩個節點是否相等的函數
    int (*match)(void *ptr,void *key);
    //鏈表長度
    unsigned long len;
}

head指向鏈表的頭節點,tail指向鏈表的尾節點,dup函數用於鏈表轉移復制時對節點value拷貝的一個實現,一般情況下使用等號足以,但在某些特殊情況下可能會用到節點轉移函數,默認可以給這個函數賦值NULL即表示使用等號進行節點轉移。free函數用於釋放一個節點所占用的內存空間,默認賦值NULL的話,即使用Redis自帶的zfree函數進行內存空間釋放。match函數是用來比較兩個鏈表節點的value值是否相等,相等返回1,不等返回0。len表示這個鏈表共有多少個節點,這樣就可以在O(1)的時間復雜度內獲得鏈表的長度。

鏈表的結構如下所示:

zipList

RediszipList結構如下所示:

typedf struct ziplist<T>{
    //壓縮列表占用字符數
    int32 zlbytes;
    //最后一個元素距離起始位置的偏移量,用於快速定位最后一個節點
    int32 zltail_offset;
    //元素個數
    int16 zllength;
    //元素內容
    T[] entries;
    //結束位 0xFF
    int8 zlend;
}ziplist

zipList的結構如下所示:

注意到zltail_offset這個參數,有了這個參數就可以快速定位到最后一個entry節點的位置,然后開始倒序遍歷,也就是說zipList支持雙向遍歷。

下面是entry的結構:

typede struct entry{
    //前一個entry的長度
    int<var> prelen;
    //元素類型編碼
    int<var> encoding;
    //元素內容
    optional byte[] content;
}entry

prelen保存的是前一個entry節點的長度,這樣在倒序遍歷時就可以通過這個參數定位到上一個entry的位置。encoding保存了content的編碼類型。content則是保存的元素內容,它是optional類型的,表示這個字段是可選的。當content是很小的整數時,它會內聯到content字段的尾部。entry結構的示意圖如下所示:

好了,那現在我們思考一個問題,為什么有了linkedList還有設計一個zipList呢?就像zipList的名字一樣,它是一個壓縮列表,是為了節約內存而開發的。相比於linkedList,其少了prenext兩個指針。在Redis中,prenext指針就要占用16個字節(64位系統的一個指針就是8個字節)。另外,linkedList的每個節點的內存都是單獨分配,加劇內存的碎片化,影響內存的管理效率。與之相對的是,zipList是由連續的內存組成的,這樣一來,由於內存是連續的,就減少了許多內存碎片和指針的內存占用,進而節約了內存。

zipList遍歷時,先根據zlbyteszltail_offset定位到最后一個entry的位置,然后再根據最后一個entry里的prelen時確定前一個entry的位置。

連鎖更新

上面說到了,entry中有一個prelen字段,它的長度要么是1個字節,要么都是5個字節:

  • 前一個節點的長度小於254個字節,則prelen長度為1字節;
  • 前一個節點的長度大於254字節,則prelen長度為5字節;

假設現在有一組壓縮列表,長度都在250~253字節之間,突然新增一個entry節點,這個entry節點長度大於等於254字節。由於新的entry節點大於等於254字節,這個entry節點的prelen為5個字節,隨后會導致其余的所有entry節點的prelen增大為5字節。

同樣地,刪除操作也會導致出現連鎖更新這種情況,假設在某一時刻,插入一個長度大於等於254個字節的entry節點,同時刪除其后面的一個長度小於254個字節的entry節點,由於小於254的entry節點的刪除,大於等於254個字節的entry節點將會與后面小於254個字節的entry節點相連,此時就與新增一個長度大於等於254個字節的entry節點時的情況一樣,將會發生連續更新。發生連續更新時,Redis需要不斷地對壓縮列表進行內存分配工作,直到結束。

linkedList與zipList的對比

  • 當列表對象中元素的長度較小或者數量較少時,通常采用zipList來存儲;當列表中元素的長度較大或者數量比較多的時候,則會轉而使用雙向鏈表linkedList來存儲。
  • 雙向鏈表linkedList便於在表的兩端進行pushpop操作,在插入節點上復雜度很低,但是它的內存開銷比較大。首先,它在每個節點上除了要保存數據之外,還有額外保存兩個指針;其次,雙向鏈表的各個節點都是單獨的內存塊,地址不連續,容易形成內存碎片。
  • zipList存儲在一塊連續的內存上,所以存儲效率很高。但是它不利於修改操作,插入和刪除操作需要頻繁地申請和釋放內存。特別是當zipList長度很長時,一次realloc可能會導致大量的數據拷貝。

quickList

Redis3.2版本之后,list的底層實現方式又多了一種,quickListqucikList是由zipList和雙向鏈表linkedList組成的混合體。它將linkedList按段切分,每一段使用zipList來緊湊存儲,多個zipList之間使用雙向指針串接起來。示意圖如下所示:

節點quickListNode的定義如下:

typedf struct quicklistNode{
    //前一個節點
    quicklistNode* prev;
    //后一個節點
    quicklistNode* next;
    //壓縮列表
    ziplist* zl;	
    //ziplist大小
    int32 size;		
    //ziplist 中元素數量
    int16 count;
    //編碼形式 存儲 ziplist 還是進行 LZF 壓縮儲存的zipList
    int2 encoding;			
    ...
}quickListNode

quickList的定義如下所示:

typedf struct quicklist{
    //指向頭結點
    quicklistNode* head;
    //指向尾節點
    quicklistNode* tail;
    //元素總數
    long count;
    //quicklistNode節點的個數
    int nodes;	
    //壓縮算法深度
    int compressDepth;		
    ...
}quickList

上述代碼簡單地表示了quickList的大致結構,為了進一步節約空間,Redis還會對zipList進行壓縮存儲,使用LZF算法進行壓縮,可以選擇壓縮深度。

每個zipList可以存儲多少個元素

想要了解這個問題,就得打開redis.conf文件了。在DVANCED CONFIG下面有着清晰的記載。

# Lists are also encoded in a special way to save a lot of space.
# The number of entries allowed per internal list node can be specified
# as a fixed maximum size or a maximum number of elements.
# For a fixed maximum size, use -5 through -1, meaning:
# -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

quickList內部默認單個zipList長度為8k字節,即list-max-ziplist-size的值設置為-2,超出了這個閾值,就會重新生成一個zipList來存儲數據。根據注釋可知,性能最好的時候就是就是list-max-ziplist-size-1-2,即分別是4kb和8kb的時候,當然,這個值也可以被設置為正數,當list-max-ziplist-szie正數n時,表示每個quickList節點上的zipList最多包含n個數據項。

壓縮深度

上面提到過,quickList中可以使用壓縮算法對zipList進行進一步的壓縮,這個算法就是LZF算法,這是一種無損壓縮算法,具體可以參考上面的鏈接。使用壓縮算法對zipList進行壓縮后,zipList的結構如下所示:

typedf struct ziplist_compressed{
    //元素個數
    int32 size;
    //元素內容
    byte[] compressed_data
}

此時quickList的示意圖如下所示:

當然,在redis.conf文件中的DVANCED CONFIG下面也可以對壓縮深度進行配置。

# Lists may also be compressed.
# Compress depth is the number of quicklist ziplist nodes from *each* side of
# the list to *exclude* from compression.  The head and tail of the list
# are always uncompressed for fast push/pop operations.  Settings are:
# 0: disable all list compression
# 1: depth 1 means "don't start compressing until after 1 node into the list,
#    going from either the head or tail"
#    So: [head]->node->node->...->node->[tail]
#    [head], [tail] will always be uncompressed; inner nodes will compress.
# 2: [head]->[next]->node->node->...->node->[prev]->[tail]
#    2 here means: don't compress head or head->next or tail->prev or tail,
#    but compress all nodes between them.
# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
# etc.
list-compress-depth 0

list-compress-depth這個參數表示一個quickList兩端不被壓縮的節點個數。需要注意的是,這里的節點個數是指quicklist雙向鏈表的節點個數,而不是指ziplist里面的數據項個數。實際上,一個quicklist節點上的ziplist,如果被壓縮,就是整體被壓縮的。

  • quickList默認的壓縮深度為0,也就是不開啟壓縮
  • list-compress-depth為1,表示quickList的兩端各有1個節點不進行壓縮,中間結點進行壓縮;
  • list-compress-depth為2,表示quickList的首尾2個節點不進行壓縮,中間結點進行壓縮;
  • 以此類推

從上面可以看出,對於quickList來說,其首尾兩個節點永遠不會被壓縮。

總結

參考

圖解redis五種數據結構底層實現

你確定不來了解一下Redis列表的內部原理

Redis 3.2版本后list的實現-quickList

Redis深度歷險:核心原理和應用實踐


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM