Redis核心原理與實踐–列表實現原理之quicklist結構
- 2021 年 9 月 19 日
- 筆記
- Redis核心原理與實踐
在上一篇文章《Redis列表實現原理之ziplist結構》,我們分析了ziplist結構如何使用一塊完整的記憶體存儲列表數據。
同時也提出了一個問題:如果鏈表很長,ziplist中每次插入或刪除節點時都需要進行大量的記憶體拷貝,這個性能是無法接受的。
本文分析quicklist結構如何解決這個問題,並實現Redis的列表類型。
quicklist的設計思想很簡單,將一個長ziplist拆分為多個短ziplist,避免插入或刪除元素時導致大量的記憶體拷貝。
ziplist存儲數據的形式更類似於數組,而quicklist是真正意義上的鏈表結構,它由quicklistNode節點鏈接而成,在quicklistNode中使用ziplist存儲數據。
提示:本文以下程式碼如無特殊說明,均位於quicklist.h/quicklist.c中。
本文以下說的「節點」,如無特殊說明,都指quicklistNode節點,而不是ziplist中的節點。
定義
quicklistNode的定義如下:
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz;
unsigned int count : 16;
unsigned int encoding : 2;
unsigned int container : 2;
unsigned int recompress : 1;
unsigned int attempted_compress : 1;
unsigned int extra : 10;
} quicklistNode;
- prev、next:指向前驅節點,後驅節點。
- zl:ziplist,負責存儲數據。
- sz:ziplist佔用的位元組數。
- count:ziplist的元素數量。
- encoding:2代表節點已壓縮,1代表沒有壓縮。
- container:目前固定為2,代表使用ziplist存儲數據。
- recompress:1代表暫時解壓(用於讀取數據等),後續需要時再將其壓縮。
- extra:預留屬性,暫未使用。
當鏈表很長時,中間節點數據訪問頻率較低。這時Redis會將中間節點數據進行壓縮,進一步節省記憶體空間。Redis採用是無損壓縮演算法—LZF演算法。
壓縮後的節點定義如下:
typedef struct quicklistLZF {
unsigned int sz;
char compressed[];
} quicklistLZF;
- sz:壓縮後的ziplist大小。
- compressed:存放壓縮後的ziplist位元組數組。
quicklist的定義如下:
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count;
unsigned long len;
int fill : QL_FILL_BITS;
unsigned int compress : QL_COMP_BITS;
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
- head、tail:指向頭節點、尾節點。
- count:所有節點的ziplist的元素數量總和。
- len:節點數量。
- fill:16bit,用於判斷節點ziplist是否已滿。
- compress:16bit,存放節點壓縮配置。
quicklist的結構如圖2-5所示。
操作分析
插入元素到quicklist頭部:
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
quicklistNode *orig_head = quicklist->head;
// [1]
if (likely(
_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
// [2]
quicklist->head->zl =
ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
// [3]
quicklistNodeUpdateSz(quicklist->head);
} else {
// [4]
quicklistNode *node = quicklistCreateNode();
node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
quicklistNodeUpdateSz(node);
_quicklistInsertNodeBefore(quicklist, quicklist->head, node);
}
quicklist->count++;
quicklist->head->count++;
return (orig_head != quicklist->head);
}
參數說明:
- value、sz:插入元素的內容與大小。
【1】判斷head節點ziplist是否已滿,_quicklistNodeAllowInsert函數中根據quicklist.fill屬性判斷節點是否已滿。
【2】head節點未滿,直接調用ziplistPush函數,插入元素到ziplist中。
【3】更新quicklistNode.sz屬性。
【4】head節點已滿,創建一個新節點,將元素插入新節點的ziplist中,再將該節點頭插入quicklist中。
也可以在quicklist的指定位置插入元素:
REDIS_STATIC void _quicklistInsert(quicklist *quicklist, quicklistEntry *entry,
void *value, const size_t sz, int after) {
int full = 0, at_tail = 0, at_head = 0, full_next = 0, full_prev = 0;
int fill = quicklist->fill;
quicklistNode *node = entry->node;
quicklistNode *new_node = NULL;
...
// [1]
if (!_quicklistNodeAllowInsert(node, fill, sz)) {
full = 1;
}
if (after && (entry->offset == node->count)) {
at_tail = 1;
if (!_quicklistNodeAllowInsert(node->next, fill, sz)) {
full_next = 1;
}
}
if (!after && (entry->offset == 0)) {
at_head = 1;
if (!_quicklistNodeAllowInsert(node->prev, fill, sz)) {
full_prev = 1;
}
}
// [2]
...
}
參數說明:
- entry:quicklistEntry結構,quicklistEntry.node指定元素插入的quicklistNode節點,quicklistEntry.offset指定插入ziplist的索引位置。
- after:是否在quicklistEntry.offset之後插入。
【1】根據參數設置以下標誌。
- full:待插入節點ziplist是否已滿。
- at_tail:是否ziplist尾插。
- at_head:是否ziplist頭插。
- full_next:後驅節點是否已滿。
- full_prev:前驅節點是否已滿。
提示:頭插指插入鏈表頭部,尾插指插入鏈表尾部。
【2】根據上面的標誌進行處理,程式碼較煩瑣,這裡不再列出。
這裡的執行邏輯如表2-2所示。
條件 | 條件說明 | 處理方式 |
---|---|---|
!full && after | 待插入節點未滿,ziplist尾插 | 再次檢查ziplist插入位置是否存在後驅元素,如果不存在則調用ziplistPush函數插入元素(更快),否則調用ziplistInsert插入元素 |
!full && !after | 待插入節點未滿,非ziplist尾插 | 調用ziplistInsert函數插入元素 |
full && at_tail && node -> next && !full_next && after | 待插入節點已滿,尾插,後驅節點未滿 | 將元素插入後驅節點ziplist中 |
full && at_head && node -> prev && !full_prev && !after | 待插入節點已滿,ziplist頭插,前驅節點未滿 | 將元素插入前驅節點ziplist中 |
full && ((at_tail && node -> next && full_next && after) ||(at_head && node->prev && full_prev && !after)) | 待插入節點已滿,尾插且後驅節點已滿,或者頭插且前驅節點已滿 | 構建一個新節點,將元素插入新節點,並根據after參數將新節點插入quicklist中 |
full | 待插入節點已滿,並且在節點ziplist中間插入 | 將插入節點的數據拆分到兩個節點中,再插入拆分後的新節點中 |
我們只看最後一種場景的實現:
// [1]
quicklistDecompressNodeForUse(node);
// [2]
new_node = _quicklistSplitNode(node, entry->offset, after);
new_node->zl = ziplistPush(new_node->zl, value, sz,
after ? ZIPLIST_HEAD : ZIPLIST_TAIL);
new_node->count++;
quicklistNodeUpdateSz(new_node);
// [3]
__quicklistInsertNode(quicklist, node, new_node, after);
// [4]
_quicklistMergeNodes(quicklist, node);
【1】如果節點已壓縮,則解壓節點。
【2】從插入節點中拆分出一個新節點,並將元素插入新節點中。
【3】將新節點插入quicklist中。
【4】嘗試合併節點。_quicklistMergeNodes嘗試執行以下操作:
- 將node->prev->prev合併到node->prev。
- 將node->next合併到node->next->next。
- 將node->prev合併到node。
- 將node合併到node->next。
合併條件:如果合併後節點大小仍滿足quicklist.fill參數要求,則合併節點。
這個場景處理與B+樹的節點分裂合併有點相似。
quicklist常用的函數如表2-3所示。
函數 | 作用 |
---|---|
quicklistCreate、quicklistNew | 創建一個空的quicklist |
quicklistPushHead,quicklistPushTail | 在quicklist頭部、尾部插入元素 |
quicklistIndex | 查找給定索引的quicklistEntry節點 |
quicklistDelEntry | 刪除給定的元素 |
配置說明
- list-max-ziplist-size:配置server.list_max_ziplist_size屬性,該值會賦值給quicklist.fill。取正值,表示quicklist節點的ziplist最多可以存放多少個元素。例如,配置為5,表示每個quicklist節點的ziplist最多包含5個元素。取負值,表示quicklist節點的ziplist最多佔用位元組數。這時,它只能取-1到-5這五個值(默認值為-2),每個值的含義如下:
-5:每個quicklist節點上的ziplist大小不能超過64 KB。
-4:每個quicklist節點上的ziplist大小不能超過32 KB。
-3:每個quicklist節點上的ziplist大小不能超過16 KB。
-2:每個quicklist節點上的ziplist大小不能超過8 KB。
-1:每個quicklist節點上的ziplist大小不能超過4 KB。 - list-compress-depth:配置server.list_compress_depth屬性,該值會賦值給quicklist.compress。
0:表示節點都不壓縮,Redis的默認配置。
1:表示quicklist兩端各有1個節點不壓縮,中間的節點壓縮。
2:表示quicklist兩端各有2個節點不壓縮,中間的節點壓縮。
3:表示quicklist兩端各有3個節點不壓縮,中間的節點壓縮。
以此類推。
編碼
ziplist由於結構緊湊,能高效使用記憶體,所以在Redis中被廣泛使用,可用於保存用戶列表、散列、有序集合等數據。
列表類型只有一種編碼格式OBJ_ENCODING_QUICKLIST,使用quicklist存儲數據(redisObject.ptr指向quicklist結構)。列表類型的實現程式碼在t_list.c中,讀者可以查看源碼了解實現更多細節。
總結
- ziplist是一種結構緊湊的數據結構,使用一塊完整記憶體存儲鏈表的所有數據。
- ziplist內的元素支援不同的編碼格式,以最大限度地節省記憶體。
- quicklist通過切分ziplist來提高插入、刪除元素等操作的性能。
- 鏈表的編碼格式只有OBJ_ENCODING_QUICKLIST。
本文內容摘自作者新書《Redis核心原理與實踐》,這本書深入地分析了Redis常用特性的內部機制與實現方式,大部分內容源自對Redis源碼的分析,並從中總結出設計思路、實現原理。通過閱讀本書,讀者可以快速、輕鬆地了解Redis的內部運行機制。
經過該書編輯同意,我會繼續在個人技術公眾號(binecy)發布書中部分章節內容,作為書的預覽內容,歡迎大家查閱,謝謝。