本文及后續文章,Redis版本均是v3.2.8
我們在使用Redis對外暴露的list數據結構時,給我們帶來極大的便利性。其底層實現所依賴的內部數據結構就是quicklist。
我們先來回憶下list這種數據結構的特點:
-
表list是一個能維持數據項先后順序的雙向鏈表
-
在表list的兩端追加和刪除數據極為方便,時間復雜度為O(1)
-
表list也支持在任意中間位置的存取操作,時間復雜度為O(N)
-
表list經常被用作隊列使用
本篇文章我們來討論下quicklist,
先來看下Redis官方quicklist.c和quicklist.h對quicklist的描述
A doubly linked list of ziplists
A generic doubly linked quicklist implementation
quicklist是一個ziplist的雙向鏈表(雙向鏈表是由多個節點Node組成的)。也就是說quicklist的每個節點都是一個ziplist。ziplist本身也是一個能維持數據項先后順序的列表(按插入位置),而且是一個各個數據項在內存上前后相鄰的列表。
一、quicklist結構定義
-
雙向鏈表在表的兩端進行push和pop操作十分的便節,但是它的內存開銷比較大。
首先,它在每個節點上除了要保存數據之外,還要額外保存兩個指針;
其次,雙向鏈表的各個節點是單獨的內存塊,地址不連續,節點多了容易產生內存碎片。
-
ziplist由於是一整塊連續內存,所以存儲效率很高。
首先,它不利於修改操作,每次數據變動都會引發一次內存的realloc。
其次,當ziplist長度很長的時候,一次realloc可能會導致大批量的數據拷貝,進一步降低性能。
Redis基於空間和時間的考慮,於是quicklist結合雙向鏈表和ziplist的優點。
/* Node, quicklist, and Iterator are the only data structures used currently. */
/* quicklist is a 32 byte struct (on 64-bit systems) describing a quicklist.
* 'count' is the number of total entries.
* 'len' is the number of quicklist nodes.
* 'compress' is: -1 if compression disabled, otherwise it's the number
* of quicklistNodes to leave uncompressed at ends of quicklist.
* 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
// 指向quicklist的頭部
quicklistNode *head;
// 指向quicklist的尾部
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned int len; /* number of quicklistNodes */
// ziplist大小限定,由list-max-ziplist-size給定
int fill : 16; /* fill factor for individual nodes */
// 節點壓縮深度設置,由list-compress-depth給定
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
* We use bit fields keep the quicklistNode at 32 bytes.
* count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
* encoding: 2 bits, RAW=1, LZF=2.
* container: 2 bits, NONE=1, ZIPLIST=2.
* recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
* attempted_compress: 1 bit, boolean, used for verifying during testing.
* extra: 12 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
// 指向上一個ziplist節點
struct quicklistNode *prev;
// 指向下一個ziplist節點
struct quicklistNode *next;
// 數據指針,如果沒有被壓縮,就指向ziplist結構,反之指向quicklistLZF結構
unsigned char *zl;
// 表示指向ziplist結構的總長度(內存占用長度)
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
// 預留字段,存放數據的方式,1--NONE,2--ziplist
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
// 解壓標記,當查看一個被壓縮的數據時,需要暫時解壓,標記此參數為1,之后再重新進行壓縮
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
// 擴展字段
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
* 'sz' is byte length of 'compressed' field.
* 'compressed' is LZF data with total (compressed) length 'sz'
* NOTE: uncompressed length is stored in quicklistNode->sz.
* When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
typedef struct quicklistLZF {
// LZF壓縮后占用的字節數
unsigned int sz; /* LZF size in bytes*/
// 柔性數組,存放壓縮后的ziplist字節數組
char compressed[];
} quicklistLZF;
注意點:
quicklistNode 中sz,如果ziplist被壓縮了,那么這個sz的值仍然是壓縮前的ziplist大小。
從上述的定義中,我們了解到quicklist 在64位系統中占用32字節的空間,quicklistNode 是一個32字節的結構。
上面提到了兩個重要參數配置:
list-max-ziplist-size
list-compress-depth
list-max-ziplist-size
1、list-max-ziplist-size取值,可以取正值,也可以取負值。
當取正值的時候,表示按照數據項個數來限定每個quicklist節點上的ziplist長度。比如,當這個參數配置成5的時候,表示每個quicklist節點的ziplist最多包含5個數據項。
當取負值的時候,表示按照占用字節數來限定每個quicklist節點上的ziplist長度。這時,它只能取-1到-5這五個值,每個值含義如下:
-
-5: 每個quicklist節點上的ziplist大小不能超過64 Kb。(注:1kb => 1024 bytes)
-
-4: 每個quicklist節點上的ziplist大小不能超過32 Kb。
-
-3: 每個quicklist節點上的ziplist大小不能超過16 Kb。
-
-2: 每個quicklist節點上的ziplist大小不能超過8 Kb。(-2是Redis給出的默認值)
-
-1: 每個quicklist節點上的ziplist大小不能超過4 Kb。
2、list-max-ziplist-size配置產生的原因?
-
每個quicklist節點上的ziplist越短,則內存碎片越多。內存碎片多了,有可能在內存中產生很多無法被利用的小碎片,從而降低存儲效率。這種情況的極端是每個quicklist節點上的ziplist只包含一個數據項,這就蛻化成一個普通的雙向鏈表了。
-
每個quicklist節點上的ziplist越長,則為ziplist分配大塊連續內存空間的難度就越大。有可能出現內存里有很多小塊的空閑空間(它們加起來很多),但卻找不到一塊足夠大的空閑空間分配給ziplist的情況。這同樣會降低存儲效率。這種情況的極端是整個quicklist只有一個節點,所有的數據項都分配在這僅有的一個節點的ziplist里面。這其實蛻化成一個ziplist了。
可見,一個quicklist節點上的ziplist要保持一個合理的長度。那到底多長合理呢?Redis提供了一個配置參數list-max-ziplist-size,就是為了讓使用者可以來根據實際應用場景進行調整優化。
list-compress-depth
其表示一個quicklist兩端不被壓縮的節點個數。注:這里的節點個數是指quicklist雙向鏈表的節點個數,而不是指ziplist里面的數據項個數。實際上,一個quicklist節點上的ziplist,如果被壓縮,就是整體被壓縮的。
1、list-compress-depth的取值:
-
0: 是個特殊值,表示都不壓縮。這是Redis的默認值。
-
1: 表示quicklist兩端各有1個節點不壓縮,中間的節點壓縮。
-
2: 表示quicklist兩端各有2個節點不壓縮,中間的節點壓縮。
-
3: 表示quicklist兩端各有3個節點不壓縮,中間的節點壓縮。
-
依此類推…
由於0是個特殊值,很容易看出quicklist的頭節點和尾節點總是不被壓縮的,以便於在表的兩端進行快速存取。
2、list-compress-depth配置產生原因?
當表list存儲大量數據的時候,最容易被訪問的很可能是兩端的數據,中間的數據被訪問的頻率比較低(訪問起來性能也很低)。如果應用場景符合這個特點,那么list還提供了一個選項,能夠把中間的數據節點進行壓縮,從而進一步節省內存空間。Redis的配置參數list-compress-depth就是用來完成這個設置的。
二、quicklist典型基本操作函數
當我們使用lpush或rpush等命令第一次向一個不存在的list里面插入數據的時候,Redis會首先調用quicklistCreate接口創建一個空的quicklist。
1、quicklist的創建
/* Create a new quicklist.
* Free with quicklistRelease(). */
quicklist *quicklistCreate(void) {
struct quicklist *quicklist;
quicklist = zmalloc(sizeof(*quicklist));
quicklist->head = quicklist->tail = NULL;
quicklist->len = 0;
quicklist->count = 0;
quicklist->compress = 0;
quicklist->fill = -2;
return quicklist;
}
從上述代碼中,我們看到quicklist是一個不包含空余頭節點的雙向鏈表(head和tail都初始化為NULL)。
2、quicklist的push操作
/* Wrapper to allow argument-based switching between HEAD/TAIL pop */
void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
int where) {
if (where == QUICKLIST_HEAD) {
quicklistPushHead(quicklist, value, sz);
} else if (where == QUICKLIST_TAIL) {
quicklistPushTail(quicklist, value, sz);
}
}
/* Add new entry to head node of quicklist.
*
* Returns 0 if used existing head.
* Returns 1 if new head created. */
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
quicklistNode *orig_head = quicklist->head;
if (likely(
_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
quicklist->head->zl =
ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
quicklistNodeUpdateSz(quicklist->head);
} else {
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);
}
/* Add new entry to tail node of quicklist.
*
* Returns 0 if used existing tail.
* Returns 1 if new tail created. */
int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
quicklistNode *orig_tail = quicklist->tail;
if (likely(
_quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
quicklist->tail->zl =
ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
quicklistNodeUpdateSz(quicklist->tail);
} else {
quicklistNode *node = quicklistCreateNode();
node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
quicklistNodeUpdateSz(node);
_quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
}
quicklist->count++;
quicklist->tail->count++;
return (orig_tail != quicklist->tail);
}
從上述的代碼中,我們可以看到
不管是在頭部還是尾部插入數據,都包含兩種情況:
-
如果頭節點(或尾節點)上ziplist大小沒有超過限制(即
_quicklistNodeAllowInsert
返回1),那么新數據被直接插入到ziplist中(調用ziplistPush
)。 -
如果頭節點(或尾節點)上ziplist太大了,那么新創建一個quicklistNode節點(對應地也會新創建一個ziplist),然后把這個新創建的節點插入到quicklist雙向鏈表中(調用
_quicklistInsertNodeAfter
)。
3、quicklist的pop操作
/* Default pop function
*
* Returns malloc'd value from quicklist */
int quicklistPop(quicklist *quicklist, int where, unsigned char **data,
unsigned int *sz, long long *slong) {
unsigned char *vstr;
unsigned int vlen;
long long vlong;
if (quicklist->count == 0)
return 0;
int ret = quicklistPopCustom(quicklist, where, &vstr, &vlen, &vlong,
_quicklistSaver);
if (data)
*data = vstr;
if (slong)
*slong = vlong;
if (sz)
*sz = vlen;
return ret;
}
quicklist的pop操作是調用quicklistPopCustom來實現的。
quicklistPopCustom的實現過程基本上跟quicklistPush相反:
首先,從頭部或尾部節點的ziplist中把對應的數據項刪除;
其次,如果在刪除后ziplist為空了,那么對應的頭部或尾部節點也要刪除;
最后,刪除后還可能涉及到里面節點的解壓縮問題。
quicklist不僅實現了從頭部或尾部插入,也實現了從任意指定的位置插入。quicklistInsertAfter和quicklistInsertBefore就是分別在指定位置后面和前面插入數據項。這種在任意指定位置插入數據的操作,情況比較復雜。
-
當插入位置所在的ziplist大小沒有超過限制時,直接插入到ziplist中就好了
-
當插入位置所在的ziplist大小超過了限制,但插入的位置位於ziplist兩端,並且相鄰的quicklist鏈表節點的ziplist大小沒有超過限制,那么就轉而插入到相鄰的那個quicklist鏈表節點的ziplist中
-
當插入位置所在的ziplist大小超過了限制,但插入的位置位於ziplist兩端,並且相鄰的quicklist鏈表節點的ziplist大小也超過限制,這時需要新創建一個quicklist鏈表節點插入
-
對於插入位置所在的ziplist大小超過了限制的其它情況(主要對應於在ziplist中間插入數據的情況),則需要把當前ziplist分裂為兩個節點,然后再其中一個節點上插入數據
三、總結
quicklist將雙向鏈表和ziplist兩者的優點結合起來,在時間和空間上做了一個均衡,能較大程度上提高Redis的效率。push和pop等操作操作的時間復雜度也都達到了最優。
--EOF--