Redis數據結構之quicklist


本文及后續文章,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--


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM