列表物件有 3 種編碼:ziplist
、linkedlist
、quicklist
。
ziplist
和linkedlist
是 3.2 版本之前的編碼。quicklist
是 3.2 版本新增的編碼,ziplist
和linkedlist
在 3.2 版本及後續版本將不再是列表物件的編碼。
編碼定義如下(server.h
):
#define OBJ_ENCODING_LINKEDLIST 4
#define OBJ_ENCODING_ZIPLIST 5
#define OBJ_ENCODING_QUICKLIST 9
雖然 ziplist
和 linkedlist
不再被列表物件作為編碼,但是我們還是有必要了解的。因為 quicklist
也是基於 ziplist
和 linkedlist
改良的。
ziplist
壓縮列表 ziplist 在之前的文章 Redis 設計與實現 5:壓縮列表 ziplist 有介紹過,結構如下:
我們使用命令操作列表的元素的時候,實際上就是在操作 entry 的資料。下面我們來舉個例子:
redis> RPUSH list_key 1 "ab" "d"
如果 list_key
用 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;
結構示意圖如下:
資料將儲存在 listNode 的 value 中,資料是一個字串物件,用 redisObject
包裹著 sds
。
例如可能是 embstr 編碼的 sds :
下面我們來舉個例子:
redis> RPUSH list_key 1 "ab" "d"
假如 list_key
的編碼是 linkedlist
,那麼結構如下圖:
quicklist
快速列表 quicklist
是 3.2
版本新新增的編碼型別,結合了 ziplist
和 linkedlist
的一種編碼。
同時在 3.2
版本中,列表也廢棄了 ziplist
和 linkedlist
。
通過上面的介紹,我們可以看出。雙向連結串列的記憶體開銷很大,每個節點的地址不連續,容易產生記憶體碎片,quicklist
利用 ziplist
減少節點數量,但 ziplist
插入和刪除數都很麻煩,複雜度高,為避免長度較長的 ziplist
修改時帶來的記憶體拷貝開銷,通過配置項配置合理的 ziplist
長度。
quicklist
的結構如下:
從上圖可以看出,quicklist
跟 linkedlist
最大的不同就是,quicklist
的值指向的是 ziplist
!ziplist
可比之前的 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
增刪的複雜度高,所以 quicklist
用 fill
引數來控制 ziplist
的大小,它是通過配置檔案的list-max-ziplist-size
配置。
- 當數字為正數,表示:每個節點的
ziplist
最多包含的entry
個數。 - 當數字為負數:
- -1:每個節點的
ziplist
位元組大小不能超過4kb - -2:每個節點的
ziplist
位元組大小不能超過8kb (redis預設值) - -3:每個節點的
ziplist
位元組大小不能超過16kb - -4:每個節點的
ziplist
位元組大小不能超過32kb - -5:每個節點的
ziplist
位元組大小不能超過64kb
- -1:每個節點的
配置二: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/quicklistPushHead
、quicklist.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 種編碼:
ziplist
、linkedlist
、quicklist
。 quicklist
是3.2
後新增的用於替代ziplist
和linkedlist
的編碼。ziplist
節省記憶體,但是太長的話效能低下。linkedlist
佔用記憶體太多。quicklist
可以看成由多個ziplist
組成的linkedlist
,效能高,節省記憶體。