列表對象有 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;
quicklist 的常用操作
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
,性能高,節省內存。