本文是《Redis內部數據結構詳解》系列的第五篇。在本文中,咱們介紹一個Redis內部數據結構——quicklist。Redis對外暴露的list數據類型,它底層實現所依賴的內部數據結構就是quicklist。 html
咱們在討論中還會涉及到兩個Redis配置(在redis.conf中的ADVANCED CONFIG部分):node
list-max-ziplist-size -2
list-compress-depth 0複製代碼
咱們在討論中會詳細解釋這兩個配置的含義。redis
注:本文討論的quicklist實現基於Redis源碼的3.2分支。算法
Redis對外暴露的上層list數據類型,常常被用做隊列使用。好比它支持的以下一些操做:數組
lpush
: 在左側(即列表頭部)插入數據。rpop
: 在右側(即列表尾部)刪除數據。rpush
: 在右側(即列表尾部)插入數據。lpop
: 在左側(即列表頭部)刪除數據。這些操做都是O(1)時間複雜度的。數據結構
固然,list也支持在任意中間位置的存取操做,好比lindex
和linsert
,但它們都須要對list進行遍歷,因此時間複雜度較高。性能
概況起來,list具備這樣的一些特色:它是一個有序列表,便於在表的兩端追加和刪除數據,而對於中間位置的存取具備O(N)的時間複雜度。這不正是一個雙向鏈表所具備的特色嗎?測試
list的內部實現quicklist正是一個雙向鏈表。在quicklist.c的文件頭部註釋中,是這樣描述quicklist的:flex
A doubly linked list of ziplistsui
它確實是一個雙向鏈表,並且是一個ziplist的雙向鏈表。
這是什麼意思呢?
咱們知道,雙向鏈表是由多個節點(Node)組成的。這個描述的意思是:quicklist的每一個節點都是一個ziplist。ziplist咱們已經在上一篇介紹過。
ziplist自己也是一個有序列表,並且是一個內存緊縮的列表(各個數據項在內存上先後相鄰)。好比,一個包含3個節點的quicklist,若是每一個節點的ziplist又包含4個數據項,那麼對外表現上,這個list就總共包含12個數據項。
quicklist的結構爲何這樣設計呢?總結起來,大概又是一個空間和時間的折中:
因而,結合了雙向鏈表和ziplist的優勢,quicklist就應運而生了。
不過,這也帶來了一個新問題:到底一個quicklist節點包含多長的ziplist合適呢?好比,一樣是存儲12個數據項,既能夠是一個quicklist包含3個節點,而每一個節點的ziplist又包含4個數據項,也能夠是一個quicklist包含6個節點,而每一個節點的ziplist又包含2個數據項。
這又是一個須要找平衡點的難題。咱們只從存儲效率上分析一下:
可見,一個quicklist節點上的ziplist要保持一個合理的長度。那到底多長合理呢?這可能取決於具體應用場景。實際上,Redis提供了一個配置參數list-max-ziplist-size
,就是爲了讓使用者能夠來根據本身的狀況進行調整。
list-max-ziplist-size -2複製代碼
咱們來詳細解釋一下這個參數的含義。它能夠取正值,也能夠取負值。
當取正值的時候,表示按照數據項個數來限定每一個quicklist節點上的ziplist長度。好比,當這個參數配置成5的時候,表示每一個quicklist節點的ziplist最多包含5個數據項。
當取負值的時候,表示按照佔用字節數來限定每一個quicklist節點上的ziplist長度。這時,它只能取-1到-5這五個值,每一個值含義以下:
另外,list的設計目標是可以用來存儲很長的數據列表的。好比,Redis官網給出的這個教程:Writing a simple Twitter clone with PHP and Redis,就是使用list來存儲相似Twitter的timeline數據。
當列表很長的時候,最容易被訪問的極可能是兩端的數據,中間的數據被訪問的頻率比較低(訪問起來性能也很低)。若是應用場景符合這個特色,那麼list還提供了一個選項,可以把中間的數據節點進行壓縮,從而進一步節省內存空間。Redis的配置參數list-compress-depth
就是用來完成這個設置的。
list-compress-depth 0複製代碼
這個參數表示一個quicklist兩端不被壓縮的節點個數。注:這裏的節點個數是指quicklist雙向鏈表的節點個數,而不是指ziplist裏面的數據項個數。實際上,一個quicklist節點上的ziplist,若是被壓縮,就是總體被壓縮的。
參數list-compress-depth
的取值含義以下:
因爲0是個特殊值,很容易看出quicklist的頭節點和尾節點老是不被壓縮的,以便於在表的兩端進行快速存取。
Redis對於quicklist內部節點的壓縮算法,採用的LZF——一種無損壓縮算法。
quicklist相關的數據結構定義能夠在quicklist.h中找到:
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
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 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
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;
typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF;
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned int len; /* number of quicklistNodes */
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;複製代碼
quicklistNode結構表明quicklist的一個節點,其中各個字段的含義以下:
zlbytes
, zltail
, zllen
, zlend
和各個數據項)。須要注意的是:若是ziplist被壓縮了,那麼這個sz的值仍然是壓縮前的ziplist大小。quicklistLZF結構表示一個被壓縮過的ziplist。其中:
真正表示quicklist的數據結構是同名的quicklist這個struct:
list-max-ziplist-size
參數的值。list-compress-depth
參數的值。上圖是一個quicklist的結構圖舉例。圖中例子對應的ziplist大小配置和節點壓縮深度配置,以下:
list-max-ziplist-size 3
list-compress-depth 2複製代碼
這個例子中咱們須要注意的幾點是:
push
和pop
操做後的一個狀態。如今咱們來大概計算一下quicklistNode結構中的count字段這16bit是否夠用。
咱們已經知道,ziplist大小受到list-max-ziplist-size
參數的限制。按照正值和負值有兩種狀況:
list-max-ziplist-size
參數是由quicklist結構的fill字段來存儲的,而fill字段是16bit,因此它所能表達的值可以用16bit來表示。prevrawlen
,1個字節的data
(len
字段和data
合二爲一;詳見上一篇)。因此,ziplist中數據項的個數不會超過32 K,用16bit來表達足夠了。實際上,在目前的quicklist的實現中,ziplist的大小還會受到另外的限制,根本不會達到這裏所分析的最大值。
下面進入代碼分析階段。
當咱們使用lpush
或rpush
命令第一次向一個不存在的list裏面插入數據的時候,Redis會首先調用quicklistCreate
接口建立一個空的quicklist。
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;
}複製代碼
在不少介紹數據結構的書上,實現雙向鏈表的時候常常會多增長一個空餘的頭節點,主要是爲了插入和刪除操做的方便。從上面quicklistCreate
的代碼能夠看出,quicklist是一個不包含空餘頭節點的雙向鏈表(head
和tail
都初始化爲NULL)。
quicklist的push操做是調用quicklistPush
來實現的。
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);
}複製代碼
不論是在頭部仍是尾部插入數據,都包含兩種狀況:
_quicklistNodeAllowInsert
返回1),那麼新數據被直接插入到ziplist中(調用ziplistPush
)。_quicklistInsertNodeAfter
)。在_quicklistInsertNodeAfter
的實現中,還會根據list-compress-depth
的配置將裏面的節點進行壓縮。它的實現比較繁瑣,咱們這裏就不展開討論了。
quicklist的操做較多,且實現細節都比較繁雜,這裏就不一一分析源碼了,咱們簡單介紹一些比較重要的操做。
quicklist的pop操做是調用quicklistPopCustom
來實現的。quicklistPopCustom
的實現過程基本上跟quicklistPush相反,先從頭部或尾部節點的ziplist中把對應的數據項刪除,若是在刪除後ziplist爲空了,那麼對應的頭部或尾部節點也要刪除。刪除後還可能涉及到裏面節點的解壓縮問題。
quicklist不只實現了從頭部或尾部插入,也實現了從任意指定的位置插入。quicklistInsertAfter
和quicklistInsertBefore
就是分別在指定位置後面和前面插入數據項。這種在任意指定位置插入數據的操做,狀況比較複雜,有衆多的邏輯分支。
quicklistSetOptions
用於設置ziplist大小配置參數(list-max-ziplist-size
)和節點壓縮深度配置參數(list-compress-depth
)。代碼比較簡單,就是將相應的值分別設置給quicklist結構的fill字段和compress字段。
下一篇咱們將介紹skiplist和它所支撐的Redis數據類型sorted set,敬請期待。