Redis 設計與實現 7:五大資料型別之列表

小新是也發表於2020-12-31

列表物件有 3 種編碼:ziplistlinkedlistquicklist

  • ziplistlinkedlist 是 3.2 版本之前的編碼。
  • quicklist 是 3.2 版本新增的編碼,ziplistlinkedlist 在 3.2 版本及後續版本將不再是列表物件的編碼。

編碼定義如下(server.h):

#define OBJ_ENCODING_LINKEDLIST 4
#define OBJ_ENCODING_ZIPLIST 5
#define OBJ_ENCODING_QUICKLIST 9

雖然 ziplistlinkedlist 不再被列表物件作為編碼,但是我們還是有必要了解的。因為 quicklist 也是基於 ziplistlinkedlist 改良的。


ziplist

壓縮列表 ziplist 在之前的文章 Redis 設計與實現 5:壓縮列表 ziplist 有介紹過,結構如下:
ziplist 的結構

我們使用命令操作列表的元素的時候,實際上就是在操作 entry 的資料。下面我們來舉個例子:

redis> RPUSH list_key 1 "ab" "d"

如果 list_keyziplist 編碼,那麼結構如下圖:
list ziplist 編碼例項結構


linkedlist

連結串列 linkedlist 的資料結構如下(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;

連結串列節點的結構也很簡單:

typedef struct listNode {
    // 前置節點
    struct listNode *prev;
    // 後置節點
    struct listNode *next;
    // 當前節點的值
    void *value;
} listNode;

結構示意圖如下:
list ziplist 編碼結構圖
資料將儲存在 listNode 的 value 中,資料是一個字串物件,用 redisObject 包裹著 sds
例如可能是 embstr 編碼的 sds :
string embstr 編碼示意圖


下面我們來舉個例子:

redis> RPUSH list_key 1 "ab" "d"

假如 list_key 的編碼是 linkedlist,那麼結構如下圖:
list linkedlist 編碼示例結構圖


quicklist

快速列表 quicklist3.2 版本新新增的編碼型別,結合了 ziplistlinkedlist 的一種編碼。
同時在 3.2 版本中,列表也廢棄了 ziplistlinkedlist

通過上面的介紹,我們可以看出。雙向連結串列的記憶體開銷很大,每個節點的地址不連續,容易產生記憶體碎片,quicklist 利用 ziplist減少節點數量,但 ziplist 插入和刪除數都很麻煩,複雜度高,為避免長度較長的 ziplist修改時帶來的記憶體拷貝開銷,通過配置項配置合理的 ziplist長度。

quicklist 的結構如下:
list quicklist 編碼結構圖
從上圖可以看出,quicklistlinkedlist 最大的不同就是,quicklist 的值指向的是 ziplistziplist 可比之前的 redisObject 節省了非常多的記憶體!
從另一個角度看,他就是把一個長的 ziplist 切割成多個小的 ziplist


程式碼實現在 quicklist.h:

typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    // 所有 ziplist 中所有的節點數
    unsigned long count;
    // quicklistNode 的數量
    unsigned long len;
    // 限定 ziplist 的最大大小,可通過配置檔案配置
    int fill : QL_FILL_BITS;
    // 壓縮程度,0 表示不壓縮,可通過配置檔案配置
    unsigned int compress : QL_COMP_BITS;
    // ...
} quicklist;

配置一:fill (控制 ziplist 大小)

太長的 ziplist 增刪的複雜度高,所以 quicklistfill 引數來控制 ziplist 的大小,它是通過配置檔案的list-max-ziplist-size配置。

  • 當數字為正數,表示:每個節點的 ziplist 最多包含的 entry 個數。
  • 當數字為負數:
    • -1:每個節點的 ziplist 位元組大小不能超過4kb
    • -2:每個節點的 ziplist 位元組大小不能超過8kb (redis預設值)
    • -3:每個節點的 ziplist 位元組大小不能超過16kb
    • -4:每個節點的 ziplist 位元組大小不能超過32kb
    • -5:每個節點的 ziplist 位元組大小不能超過64kb

配置二:compress (控制壓縮程度)

因為連結串列的特性,一般首尾兩端操作較頻繁,中部操作相對較少,所以 redis 提供壓縮深度配置:list-compress-depth,也就是屬性 compress

  • 0:表示都不壓縮。這是Redis的預設值。
  • 1:表示 quicklist 兩端各有1個節點不壓縮,中間的節點壓縮。
  • 2:表示 quicklist 兩端各有2個節點不壓縮,中間的節點壓縮。
  • 3:表示 quicklist 兩端各有3個節點不壓縮,中間的節點壓縮。

quicklist 節點

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    // 不設定壓縮資料引數 recompress 時指向一個 ziplist 結構
    // 設定壓縮資料引數recompress 時指向 quicklistLZF 結構
    unsigned char *zl;
    // ziplist 的位元組數
    unsigned int sz;
    // ziplist 中包含的節點數量
    unsigned int count : 16;
    // 編碼。1 表示壓縮過,2 表示沒壓縮
    unsigned int encoding : 2;
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    // 標記 quicklist 節點的 ziplist 之前是否被解壓縮過
    // 如果recompress 為 1,則等待被再次壓縮
    unsigned int recompress : 1;
    // ...
} quicklistNode;

壓縮過的 ziplist 結構

typedef struct quicklistLZF {
    // 表示被 LZF 演算法壓縮後的 ziplist 的大小
    unsigned int sz;
    // 壓縮後的 ziplist 的陣列,柔性陣列
    char compressed[];
} quicklistLZF;

quick 的常用操作

1. 插入

(1) quicklist 可以在頭部或者尾部插入資料:quicklist.c/quicklistPushHeadquicklist.c/quicklistPushTail,我們就挑一個從頭部插入的程式碼來看看吧(插入尾部的程式碼也是差不多的)(程式碼格式略微調整了一下):

int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_head = quicklist->head;
    // 判斷頭結點上的 ziplist 大小是否沒超過限制
    if (likely(_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
    	// 沒超過限制,就插入到 ziplist 中。ziplistPush 是 ziplist.c 的方法
        quicklist->head->zl = ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
        quicklistNodeUpdateSz(quicklist->head);
    } else {
    	// ziplist 超過大小限制,則創新建立一個新的 quicklistNode
        quicklistNode *node = quicklistCreateNode();
        // 再建立新的 ziplist,然後把 ziplist 放到節點中
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
        quicklistNodeUpdateSz(node);
        // 新的 quicklistNode 插入原來的頭結點上,成為新的頭結點
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    quicklist->count++;
    quicklist->head->count++;
    return (orig_head != quicklist->head);
}

(2) quicklist 也可以從任意指定的位置插入:quicklist.c/_quicklistInsert,實現相對來說比較複雜,我們就用文字說明(程式碼太長,感興趣的讀者自己去讀吧):

  • 當前節點是 NULL:建立一個新的節點,插入就好。
  • 當前節點的 ziplist 大小沒有超過限制時:直接插入到 ziplist 就好。
  • 當前節點的 ziplist 大小超過限制時:
    • 如果插入的位置是 ziplist兩端
      • 如果相鄰的節點的 ziplist 大小沒有超過限制,那麼就插入到相鄰節點ziplist 中。
      • 如果相鄰的節點的 ziplist 大小也超過限制,這時需要建立一個新的節點插入。
    • 如果插入的位置是 ziplist中間
      則需要把當前 ziplist 從插入位置 分裂 (_quicklistSplitNode) 為兩個節點,然後把資料插入第二個節點上。

2. 查詢

quicklist 支援通過 index 查詢元素:quicklist.c/quicklistIndex
查詢的本質就是遍歷,先檢視quicklistNode 的長度判斷 index 是否在這個節點中,如果不是則跳到下個節點。
當定位到節點之後,對節點裡面的 ziplist 進行遍歷查詢 (ziplistIndex)。

3 刪除

(1) 指定值的刪除,quicklist.c/quicklistDelEntry
這個指定的值的資訊 quicklistEntry 的結構如下:

typedef struct quicklistEntry {
    // 指向當前 quicklist 的指標
    const quicklist *quicklist;
    // 指向當前 quicklistNode 節點的指標
    quicklistNode *node;
    // 指向當前 ziplist 的指標
    unsigned char *zi;
    // 指向當前 ziplist 的字串 vlaue 成員
    unsigned char *value;
    // 當前 ziplist 的整數 value 成員
    long long longval;
    // 當前 ziplist 的位元組數大小
    unsigned int sz;
    // 在 ziplist 的偏移量
    int offset;
} quicklistEntry;

具體的刪除程式碼如下(做了一些刪減):

void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry) {
    quicklistNode *prev = entry->node->prev;
    quicklistNode *next = entry->node->next;
    // 通過 quicklistEntry 可以定位到 ziplist 中的元素位置,然後進行刪除
    // quicklist -> quicklistNode -> ziplist -> ziplistEntry
    int deleted_node = quicklistDelIndex((quicklist *)entry->quicklist, entry->node, &entry->zi);
    // 下面是迭代器的引數調整,此處忽略...
}

(2) 區間元素 index 刪除: quicklist.c/quicklistDelRange(程式碼太長了,就不晾出來了)
先通過遍歷找元素,會判斷是否可以刪除整個節點 entry.offset == 0 && extent >= node->count,可以的話不用遍歷裡面的ziplist直接刪除整個節點。
否則計算出當前節點ziplist 要刪除的範圍,通過 ziplistDeleteRange 函式刪除。


重點回顧

  • 列表物件有 3 種編碼:ziplistlinkedlistquicklist
  • quicklist3.2 後新增的用於替代 ziplistlinkedlist 的編碼。
  • ziplist 節省記憶體,但是太長的話效能低下。linkedlist 佔用記憶體太多。
  • quicklist 可以看成由多個 ziplist 組成的 linkedlist,效能高,節省記憶體。

相關文章