Redis 源碼解析 7:五大數據類型之列表


列表對象有 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;

quicklist 的常用操作

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,性能高,節省內存。


免責聲明!

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



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