redis數據結構 (三) - 鏈表

基於redis5.0的版本。

redis鏈表(List)字符編碼有:ziplist和quicklist,老版本也有linkedlis。html

1. linkedlist

3.2版本以後列表再也不使用linkedlist,這裏列出來是爲了後面的對比。node

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

struct listNode {
    listNode *prev;
    listNode *next;
    void *value;
}

e362b4b6-1789-45e3-8cfa-5ebe978e5b93.png

  • 雙向:鏈表帶有prev和next指針,獲取某個節點的前置節點和後置節點的複雜度都是O(1)。
  • 無環:表頭節點的prev指針和表尾節點的next指針都指向NULL,對列表的範圍以NULL爲終點。
  • 帶表頭指針和尾指針:經過head和taiil指針,獲取頭結點和尾節點的複雜度爲O(1)。
  • 帶鏈表長度計數器:經過len獲取節點數量的複雜度爲O(1)。
  • 多態:鏈表節點使用void*指針來保存節點值,而且能夠經過list結構的dump、free、match三個屬性爲節點值設置類型特定函數,因此鏈表能夠用於保存各類不一樣類型的值。
StringObject就是type爲string的RedisObject對象,在本文中都簡稱爲StringObject。

2. ziplist

// ziplist.c
unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0;
    zl[bytes-1] = ZIP_END;
    return zl;
}

2809985d-2d0a-449b-9a10-2ac175e39269.png

字段 類型 長度 說明
zlbytes uint32_t 4字節 記錄整個壓縮列表佔用的內存字節數,包括 4 字節的 zlbytes 自己。在對壓縮列表進行內存重分配時,或者計算zlend的位置時使用。
zltail uint32_t 4字節 記錄壓縮列表表尾節距離起始地址的字節偏移量,經過這個偏移量,能夠快速肯定最後一個節點的地址。
zllen uint16_t 2字節 記錄壓縮列表中節點的數量。當節點數量超過或者等於 UINT16_MAX(65535) 時,須要遍歷整個列表才能知道節點的數量。
entry zlentry 根據節點內容 壓縮列表包含的各個節點。
zlend uint8_t 1字節 固定值爲0xFF(255),標識壓縮列表的尾節點。其餘普通的節點不會以 255 開頭。所以能夠經過檢查節點的地一個字節是否等於 255 從而知道是否已經到達列表的末尾。

下圖實例:
a66f7c8d-f2e6-462d-b722-02c59304a9cc.png
說明:git

  1. zlbytes值爲80,標識列表總長80個字節。
  2. zltail值爲60,表示從首節點指針p加上偏移量60,便可獲得尾節點entry3的起始地址。
  3. zllen值爲3,表示entry節點數是3個。

entry:
53997048.pnggithub

2.1 prevrawlen:

prevrawlen記錄前一個節點的長度,屬性的長度是1字節或者5字節。redis

  1. 若是前一個節點的長度小於254字節,prevrawlen長度爲1字節,值爲前一個節點的長度。
  2. 若是前一個字節的長度大於254字節,則prevrawlen長度爲5字節,第一個字節值是0xFE(十進制254),以後的四個字節值是前一個節點的長度。

由於prevrawlen記錄了前一個節點的長度,因此程序能夠經過指針運算,根據當前節點的起始地址來計算出前一個節點的起始地址。壓縮列表的從表尾到表頭遍歷操做就是使用這一原理。算法

2.2 encoding

encoding記錄的是data數據的類型和長度(github的ziplist.c上有詳細說明)。數組

  1. 當最高位爲00時,encoding長度爲1字節,data字節數組長度小於63字節(2的6次方),除去最高兩位後的值表示data的長度。
  2. 當最高位爲01時,encoding長度爲2字節,data字節數組長度小於16,383字節(2的14次方),除去最高兩位後的值表示data的長度。
  3. 當最高位爲10時,encoding長度爲5字節,data字節數組長度小於4,294,967,295字節(2的36次方),除去最高兩位後的值表示data的長度。

4.當最高位爲11時,encoding長度爲1字節,data保存當是整數值:數據結構

  • 值爲11000000,data數值類型是2個字節的int16_t編碼。
  • 值爲11010000,data數值類型是4個字節的int32_t編碼。
  • 值爲11100000,data數值類型是8個字節的int64_t編碼。
  • 值爲11110000,data數值爲3個字節(24位)當有符號整數編碼。
  • 值爲1111XXXX,XXXX取值0001 ~ 1101,分別表示0~12整數值,0001表示0,以此類推,當encoding值爲這個範圍是,XXXX值即爲data值,也就是entry沒有data屬性。
  • 值爲11111110,data數值類型是1個字節(24位)當有符號整數編碼。
  • 注:encoding沒有11111111值,由於11111111固定爲ziplist的zlend值(尾節點)。

例如:函數

  • 值爲「hello」的entry:

54064527.png

  • 值爲整數「2」的entry:

54307048.png

2.3 連鎖更新

當ziplist插入新節點,或者節點內容變長時,須要追加申請須要的內存空間(ziplist.c文件下的ziplistResize函數,最終是調用object.c下的zrealloc函數),若是沒法追加申請到足夠的內存,則會從新申請一個完整的內存,並將當前ziplist數據複製到新內存空間。
prevlen屬性記錄了前一個節點的長度:假設entry2的前一個節點entry1節點長度小於254字節,則entry2的prevlen只須要1個字節來保存這個長度;若是entry1內容變動(或者在entry1和entry2之間插入新節點;或者刪除entry1使得entry2的前置節點變成entry0),超出來254字節,則entry2的prevlen當前1個字節沒法保存,須要擴展成5個字節,redis須要從新申請內存空間;若是恰好entry2本來的長度介於250~253字節之間,擴展以後,entry2的長度超出來254字節,會致使entry3也出現變動的狀況。最壞狀況下,若是每一個節點都是相似於entry1和entry2的狀況,redis須要不斷地對壓縮列表執行空間從新分配操做(ziplist.c下的__ziplistCascadeUpdate函數,while循環節點,每一個節點都會從新一次申請內存空間)
儘管連鎖更新的複雜度高,會形成性能問題,可是它出現對概率很低。post

2.4 優缺點
  • linkedlist的prev和next指針會佔用16個字節,每一個listNode內存都是單獨分配,會加重內存的碎片化。
  • ziplist是一塊連續內存,存儲效率很高,可是不利於修改,一次realloc可能會致使大批量的數據拷貝,特別是當ziplist長度很長時,進一步下降性能。

3. Quecklist

quicklist是redis list的內部實現,是一個ziplist的雙向鏈表:quecklist的每一個節點都是一個ziplist,結合了linkedlist和ziplist的優勢。

// qicklist.h
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
 * We use bit fields keep the quicklistNode at 32 bytes.
 * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
 * encoding: 2 bits, RAW=1, LZF=2.
 * container: 2 bits, NONE=1, ZIPLIST=2.
 * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
 * attempted_compress: 1 bit, boolean, used for verifying during testing.
 * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
    struct quicklistNode *prev; // 指向上一個ziplist節點
    struct quicklistNode *next; // 指向下一個ziplist節點
    unsigned char *zl; // 數據指針,若是沒有被壓縮,就指向ziplist結構,反之指向quicklistLZF結構 
    unsigned int sz; // 表示指向ziplist結構的總長度(內存佔用長度)
    unsigned int count : 16; // 表示ziplist中的數據項個數
    unsigned int encoding : 2; // 編碼方式,1--ziplist,2--quicklistLZF
    unsigned int container : 2; // 預留字段,存放數據的方式,1--NONE,2--ziplist。原本設計是用來代表一個quicklist節點下面是直接存數據,仍是使用ziplist存數據,或者用其它的結構來存數據(用做一個數據容器,因此叫container)。在目前的實現中,這個值是一個固定的值2,表示使用ziplist做爲數據容器。
    unsigned int recompress : 1; // 解壓標記,當查看一個被壓縮的數據時,須要暫時解壓,標記此參數爲1,以後再從新進行壓縮
    unsigned int attempted_compress : 1; // 測試相關
    unsigned int extra : 10; // 擴展字段,暫時沒用
} quicklistNode;
/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
 * 'sz' is byte length of 'compressed' field.
 * 'compressed' is LZF data with total (compressed) length 'sz'
 * NOTE: uncompressed length is stored in quicklistNode->sz.
 * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
typedef struct quicklistLZF { // 表示一個被壓縮過的ziplist
    unsigned int sz; // LZF壓縮後佔用的字節數
    char compressed[]; // 柔性數組,存放壓縮後的ziplist字節數組
} quicklistLZF;
/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
 * 'count' is the number of total entries.
 * 'len' is the number of quicklist nodes.
 * 'compress' is: -1 if compression disabled, otherwise it's the number
 * of quicklistNodes to leave uncompressed at ends of quicklist.
 * 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
    quicklistNode *head; // 指向quicklist的頭部節點
    quicklistNode *tail; // 指向quicklist的尾部節點
    unsigned long count; // 列表中全部數據項的個數總和
    unsigned int len; // 全部ziplist的個數總和
    int fill : 16; // ziplist大小限定,由list-max-ziplist-size給定
    unsigned int compress : 16; // 節點壓縮深度設置,由list-compress-depth給定
} quicklist;

到底一個quicklist節點包含多長的ziplist合適呢?好比,一樣是存儲12個數據項,既能夠是一個quicklist包含3個節點,而每一個節點的ziplist又包含4個數據項,也能夠是一個quicklist包含6個節點,而每一個節點的ziplist又包含2個數據項。
這又是一個須要找平衡點的難題。咱們只從存儲效率上分析一下:

  • 每一個quicklist節點上的ziplist越短,則內存碎片越多。內存碎片多了,有可能在內存中產生不少沒法被利用的小碎片,從而下降存儲效率。這種狀況的極端是每一個quicklist節點上的ziplist只包含一個數據項,這就蛻化成一個普通的雙向鏈表了。
  • 每一個quicklist節點上的ziplist越長,則爲ziplist分配大塊連續內存空間的難度就越大。有可能出現內存裏有不少小塊的空閒空間(它們加起來不少),但卻找不到一塊足夠大的空閒空間分配給ziplist的狀況。這一樣會下降存儲效率。這種狀況的極端是整個quicklist只有一個節點,全部的數據項都分配在這僅有的一個節點的ziplist裏面。這其實蛻化成一個ziplist了。

可見,一個quicklist節點上的ziplist要保持一個合理的長度。那到底多長合理呢?這可能取決於具體應用場景。實際上,Redis提供了一個配置參數list-max-ziplist-size,就是爲了讓使用者能夠來根據本身的狀況進行調整。
咱們來詳細解釋一下這個參數的含義。它能夠取正值,也能夠取負值。
當取正值的時候,表示按照數據項個數來限定每一個quicklist節點上的ziplist長度。好比,當這個參數配置成5的時候,表示每一個quicklist節點的ziplist最多包含5個數據項。
當取負值的時候,表示按照佔用字節數來限定每一個quicklist節點上的ziplist長度。這時,它只能取-1到-5這五個值,每一個值含義以下:

  • -5: 每一個quicklist節點上的ziplist大小不能超過64 Kb。(注:1kb => 1024 bytes)
  • -4: 每一個quicklist節點上的ziplist大小不能超過32 Kb。
  • -3: 每一個quicklist節點上的ziplist大小不能超過16 Kb。
  • -2: 每一個quicklist節點上的ziplist大小不能超過8 Kb。(-2是Redis給出的默認值)
  • -1: 每一個quicklist節點上的ziplist大小不能超過4 Kb。

另外,list的設計目標是可以用來存儲很長的數據列表的。好比,Redis官網給出的這個教程:Writing a simple Twitter clone with PHP and Redis,就是使用list來存儲相似Twitter的timeline數據。
當列表很長的時候,最容易被訪問的極可能是兩端的數據,中間的數據被訪問的頻率比較低(訪問起來性能也很低)。若是應用場景符合這個特色,那麼list還提供了一個選項,可以把中間的數據節點進行壓縮,從而進一步節省內存空間。Redis的配置參數list-compress-depth就是用來完成這個設置的。
這個參數表示一個quicklist兩端不被壓縮的節點個數。注:這裏的節點個數是指quicklist雙向鏈表的節點個數,而不是指ziplist裏面的數據項個數。實際上,一個quicklist節點上的ziplist,若是被壓縮,就是總體被壓縮的。
參數list-compress-depth的取值含義以下:

  • 0: 是個特殊值,表示都不壓縮。這是Redis的默認值。
  • 1: 表示quicklist兩端各有1個節點不壓縮,中間的節點壓縮。
  • 2: 表示quicklist兩端各有2個節點不壓縮,中間的節點壓縮。
  • 3: 表示quicklist兩端各有3個節點不壓縮,中間的節點壓縮。
  • 依此類推…

因爲0是個特殊值,很容易看出quicklist的頭節點和尾節點老是不被壓縮的,以便於在表的兩端進行快速存取。
Redis對於quicklist內部節點的壓縮算法,採用的LZF——一種無損壓縮算法。

// server.h
/* List defaults */
#define OBJ_LIST_MAX_ZIPLIST_SIZE -2
#define OBJ_LIST_COMPRESS_DEPTH 0
以上內容參考自:
《redis設計與實現》
Redis源碼剖析系列
Redis內部數據結構詳解系列
多是目前最詳細的Redis內存模型及應用解讀
相關文章
相關標籤/搜索