Redis源碼剖析和註釋(七)--- 快速列表(quicklist)

Redis 快速列表(quicklist)

1. 介紹

quicklist結構是在redis 3.2版本中新加的數據結構,用在列表的底層實現。

通過列表鍵查看一下:redis 列表鍵命令詳解

127.0.0.1:6379> RPUSH list 1 2 5 1000
"redis" "quicklist"(integer) 
127.0.0.1:6379> OBJECT ENCODING list
"quicklist"

quicklist結構在quicklist.c中的解釋爲A doubly linked list of ziplists意思爲一個由ziplist組成的雙向鏈表

關於ziplist結構的剖析和註釋:redis 壓縮列表ziplist結構詳解

首先回憶下壓縮列表的特點:

  • 壓縮列表ziplist結構本身就是一個連續的內存塊,由表頭、若干個entry節點和壓縮列表尾部標識符zlend組成,通過一系列編碼規則,提高內存的利用率,使用於存儲整數和短字符串
  • 壓縮列表ziplist結構的缺點是:每次插入或刪除一個元素時,都需要進行頻繁的調用realloc()函數進行內存的擴展或減小,然後進行數據”搬移”,甚至可能引發連鎖更新,造成嚴重效率的損失。

接下來介紹quicklist與ziplist的關係:

之前提到,quicklist是由ziplist組成的雙向鏈表,鏈表中的每一個節點都以壓縮列表ziplist的結構保存着數據,而ziplist有多個entry節點,保存着數據。相當與一個quicklist節點保存的是一片數據,而不再是一個數據

例如:一個quicklist有4個quicklist節點,每個節點都保存着1個ziplist結構,每個ziplist的大小不超過8kb,ziplist的entry節點中的value成員保存着數據。

根據以上描述,總結出一下quicklist的特點:

  • quicklist宏觀上是一個雙向鏈表,因此,它具有一個雙向鏈表的有點,進行插入或刪除操作時非常方便,雖然複雜度爲O(n),但是不需要內存的複製,提高了效率,而且訪問兩端元素複雜度爲O(1)。
  • quicklist微觀上是一片片entry節點,每一片entry節點內存連續且順序存儲,可以通過二分查找log2(n) 的複雜度進行定位

總體來說,quicklist給人的感覺和B樹每個節點的存儲方式相似。B 樹 - wiki

2. quicklist的結構實現

quicklist有關的數據結構定義在quicklist.h中。

2.1 quicklist表頭結構

typedef struct quicklist {
    //指向頭部(最左邊)quicklist節點的指針
    quicklistNode *head;

    //指向尾部(最右邊)quicklist節點的指針
    quicklistNode *tail;

    //ziplist中的entry節點計數器
    unsigned long count;        /* total count of all entries in all ziplists */

    //quicklist的quicklistNode節點計數器
    unsigned int len;           /* number of quicklistNodes */

    //保存ziplist的大小,配置文件設定,佔16bits
    int fill : 16;              /* fill factor for individual nodes */

    //保存壓縮程度值,配置文件設定,佔16bits,0表示不壓縮
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;

在quicklist表頭結構中,有兩個成員是fill和compress,其中” : “是位域運算符,表示fill佔int類型32位中的16位,compress也佔16位。

fill和compress的配置文件是redis.conf。

  • fill成員對應的配置:list-max-ziplist-size -2
    • 當數字爲負數,表示以下含義:
    • -1 每個quicklistNode節點的ziplist字節大小不能超過4kb。(建議)
    • -2 每個quicklistNode節點的ziplist字節大小不能超過8kb。(默認配置)
    • -3 每個quicklistNode節點的ziplist字節大小不能超過16kb。(一般不建議)
    • -4 每個quicklistNode節點的ziplist字節大小不能超過32kb。(不建議)
    • -5 每個quicklistNode節點的ziplist字節大小不能超過64kb。(正常工作量不建議)
    • 當數字爲正數,表示:ziplist結構所最多包含的entry個數。最大值爲 215
  • compress成員對應的配置:list-compress-depth 0
    • 後面的數字有以下含義:
    • 0 表示不壓縮。(默認)
    • 1 表示quicklist列表的兩端各有1個節點不壓縮,中間的節點壓縮。
    • 2 表示quicklist列表的兩端各有2個節點不壓縮,中間的節點壓縮。
    • 3 表示quicklist列表的兩端各有3個節點不壓縮,中間的節點壓縮。
    • 以此類推,最大爲 216

2.2 quicklist節點結構

typedef struct quicklistNode {
    struct quicklistNode *prev;     //前驅節點指針
    struct quicklistNode *next;     //後繼節點指針

    //不設置壓縮數據參數recompress時指向一個ziplist結構
    //設置壓縮數據參數recompress指向quicklistLZF結構
    unsigned char *zl;

    //壓縮列表ziplist的總長度
    unsigned int sz;                  /* ziplist size in bytes */

    //ziplist中包的節點數,佔16 bits長度
    unsigned int count : 16;          /* count of items in ziplist */

    //表示是否採用了LZF壓縮算法壓縮quicklist節點,1表示壓縮過,2表示沒壓縮,佔2 bits長度
    unsigned int encoding : 2;        /* RAW==1 or LZF==2 */

    //表示一個quicklistNode節點是否採用ziplist結構保存數據,2表示壓縮了,1表示沒壓縮,默認是2,佔2bits長度
    unsigned int container : 2;       /* NONE==1 or ZIPLIST==2 */

    //標記quicklist節點的ziplist之前是否被解壓縮過,佔1bit長度
    //如果recompress爲1,則等待被再次壓縮
    unsigned int recompress : 1; /* was this node previous compressed? */

    //測試時使用
    unsigned int attempted_compress : 1; /* node can't compress; too small */

    //額外擴展位,佔10bits長度
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

2.3 壓縮過的ziplist結構—quicklistLZF

當指定使用lzf壓縮算法壓縮ziplist的entry節點時,quicklistNode結構的zl成員指向quicklistLZF結構

typedef struct quicklistLZF {
    //表示被LZF算法壓縮後的ziplist的大小
    unsigned int sz; /* LZF size in bytes*/

    //保存壓縮後的ziplist的數組,柔性數組
    char compressed[];
} quicklistLZF;

2.4 管理ziplist信息的結構quicklistEntry

和壓縮列表一樣,entry結構在儲存時是一連串的內存塊,需要將其每個entry節點的信息讀取到管理該信息的結構體中,以便操作。在quicklist中定義了自己的結構。

//管理quicklist中quicklistNode節點中ziplist信息的結構
typedef struct quicklistEntry {
    const quicklist *quicklist;   //指向所屬的quicklist的指針
    quicklistNode *node;          //指向所屬的quicklistNode節點的指針
    unsigned char *zi;            //指向當前ziplist結構的指針
    unsigned char *value;         //指向當前ziplist結構的字符串vlaue成員
    long long longval;            //指向當前ziplist結構的整數value成員
    unsigned int sz;              //保存當前ziplist結構的字節數大小
    int offset;                   //保存相對ziplist的偏移量
} quicklistEntry;

基於以上結構信息,我們可以得出一個quicklist結構,在空間中的大致可能的樣子:

這裏寫圖片描述

2.5 迭代器結構實現

在redis的quicklist結構中,實現了自己的迭代器,用於遍歷節點。

//quicklist的迭代器結構
typedef struct quicklistIter {
    const quicklist *quicklist;   //指向所屬的quicklist的指針
    quicklistNode *current;       //指向當前迭代的quicklist節點的指針
    unsigned char *zi;            //指向當前quicklist節點中迭代的ziplist
    long offset;                  //當前ziplist結構中的偏移量      /* offset in current ziplist */
    int direction;                //迭代方向
} quicklistIter;

3. quicklist的部分操作源碼註釋

quicklist.c和quicklist.h文件的註釋:redis 源碼註釋

3.1 插入一個entry節點

quicklist的插入:以一個已存在的entry前或後插入一個entry節點,非常的複雜,因爲情況非常多。

  • 當前quicklistNode節點的ziplist可以插入。
    • 插入在已存在的entry前
    • 插入在已存在的entry後
  • 如果當前quicklistNode節點的ziplist由於fill的配置,無法繼續插入
    • 已存在的entry是ziplist的頭節點,當前quicklistNode節點前驅指針不爲空,且是尾插
    • 前驅節點可以插入,因此插入在前驅節點的尾部。
    • 前驅節點不可以插入,因此要在當前節點和前驅節點之間新創建一個新節點保存要插入的entry。
    • 已存在的entry是ziplist的尾節點,當前quicklistNode節點後繼指針不爲空,且是前插
    • 後繼節點可以插入,因此插入在前驅節點的頭部。
    • 後繼節點不可以插入,因此要在當前節點和後繼節點之間新創建一個新節點保存要插入的entry。
    • 以上情況不滿足,則屬於將entry插入在ziplist中間的任意位置,需要分割當前quicklistNode節點。最後如果能夠合併,還要合併。
/* Insert a new entry before or after existing entry 'entry'.
 *
 * If after==1, the new value is inserted after 'entry', otherwise
 * the new value is inserted before 'entry'. */
//如果after爲1,在已存在的entry後插入一個entry,否則在前面插入
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;

    if (!node) {    //如果entry爲沒有所屬的quicklistNode節點,需要新創建
        /* we have no reference node, so let's create only node in the list */
        D("No node given!");
        new_node = quicklistCreateNode();   //創建一個節點
        //將entry值push到new_node新節點的ziplist中
        new_node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
        //將新的quicklistNode節點插入到quicklist中
        __quicklistInsertNode(quicklist, NULL, new_node, after);
        //更新entry計數器
        new_node->count++;
        quicklist->count++;
        return;
    }

    /* Populate accounting flags for easier boolean checks later */
    //如果node不能插入entry
    if (!_quicklistNodeAllowInsert(node, fill, sz)) {
        D("Current node is full with count %d with requested fill %lu",
          node->count, fill);
        full = 1;   //設置full的標誌
    }

    //如果是後插入且當前entry爲尾部的entry
    if (after && (entry->offset == node->count)) {
        D("At Tail of current ziplist");
        at_tail = 1;    //設置在尾部at_tail標示
        //如果node的後繼節點不能插入
        if (!_quicklistNodeAllowInsert(node->next, fill, sz)) {
            D("Next node is full too.");
            full_next = 1;  //設置標示
        }
    }

    //如果是前插入且當前entry爲頭部的entry
    if (!after && (entry->offset == 0)) {
        D("At Head");
        at_head = 1;    //設置at_head表示
        if (!_quicklistNodeAllowInsert(node->prev, fill, sz)) { //如果node的前驅節點不能插入
            D("Prev node is full too.");
            full_prev = 1;      //設置標示
        }
    }

    /* Now determine where and how to insert the new element */
    //如果node不滿,且是後插入
    if (!full && after) {
        D("Not full, inserting after current position.");
        quicklistDecompressNodeForUse(node);    //將node臨時解壓
        unsigned char *next = ziplistNext(node->zl, entry->zi); //返回下一個entry的地址
        if (next == NULL) { //如果next爲空,則直接在尾部push一個entry
            node->zl = ziplistPush(node->zl, value, sz, ZIPLIST_TAIL);
        } else {            //否則,後插入一個entry
            node->zl = ziplistInsert(node->zl, next, value, sz);
        }
        node->count++;  //更新entry計數器
        quicklistNodeUpdateSz(node);    //更新ziplist的大小sz
        quicklistRecompressOnly(quicklist, node);   //將臨時解壓的重壓縮

    //如果node不滿且是前插
    } else if (!full && !after) {
        D("Not full, inserting before current position.");
        quicklistDecompressNodeForUse(node);    //將node臨時解壓
        node->zl = ziplistInsert(node->zl, entry->zi, value, sz);   //前插入
        node->count++;  //更新entry計數器
        quicklistNodeUpdateSz(node);     //更新ziplist的大小sz
        quicklistRecompressOnly(quicklist, node);   //將臨時解壓的重壓縮

    //當前node滿了,且當前已存在的entry是尾節點,node的後繼節點指針不爲空,且node的後驅節點能插入
    //本來要插入當前node中,但是當前的node滿了,所以插在next節點的頭部
    } else if (full && at_tail && node->next && !full_next && after) {
        /* If we are: at tail, next has free space, and inserting after:
         *   - insert entry at head of next node. */
        D("Full and tail, but next isn't full; inserting next node head");
        new_node = node->next;  //new_node指向node的後繼節點
        quicklistDecompressNodeForUse(new_node);    //將node臨時解壓
        new_node->zl = ziplistPush(new_node->zl, value, sz, ZIPLIST_HEAD);  //在new_node頭部push一個entry
        new_node->count++;  //更新entry計數器
        quicklistNodeUpdateSz(new_node);    //更新ziplist的大小sz
        quicklistRecompressOnly(quicklist, new_node);   //將臨時解壓的重壓縮

    //當前node滿了,且當前已存在的entry是頭節點,node的前驅節點指針不爲空,且前驅節點可以插入
    //因此插在前驅節點的尾部
    } else if (full && at_head && node->prev && !full_prev && !after) {
        /* If we are: at head, previous has free space, and inserting before:
         *   - insert entry at tail of previous node. */
        D("Full and head, but prev isn't full, inserting prev node tail");
        new_node = node->prev;  //new_node指向node的後繼節點
        quicklistDecompressNodeForUse(new_node);    //將node臨時解壓
        new_node->zl = ziplistPush(new_node->zl, value, sz, ZIPLIST_TAIL);//在new_node尾部push一個entry
        new_node->count++;  //更新entry計數器
        quicklistNodeUpdateSz(new_node);    //更新ziplist的大小sz
        quicklistRecompressOnly(quicklist, new_node);   //將臨時解壓的重壓縮

    //當前node滿了
    //要麼已存在的entry是尾節點,且後繼節點指針不爲空,且後繼節點不可以插入,且要後插
    //要麼已存在的entry爲頭節點,且前驅節點指針不爲空,且前驅節點不可以插入,且要前插
    } else if (full && ((at_tail && node->next && full_next && after) ||
                        (at_head && node->prev && full_prev && !after))) {
        /* If we are: full, and our prev/next is full, then:
         *   - create new node and attach to quicklist */
        D("\tprovisioning new node...");
        new_node = quicklistCreateNode();   //創建一個節點
        new_node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);  //將entrypush到new_node的頭部
        new_node->count++;  //更新entry計數器
        quicklistNodeUpdateSz(new_node);        //更新ziplist的大小sz
        __quicklistInsertNode(quicklist, node, new_node, after);    //將new_node插入在當前node的後面

    //當前node滿了,且要將entry插入在中間的任意地方,需要將node分割
    } else if (full) {
        /* else, node is full we need to split it. */
        /* covers both after and !after cases */
        D("\tsplitting node...");
        quicklistDecompressNodeForUse(node);    //將node臨時解壓
        new_node = _quicklistSplitNode(node, entry->offset, after);//分割node成兩塊
        new_node->zl = ziplistPush(new_node->zl, value, sz,
                                   after ? ZIPLIST_HEAD : ZIPLIST_TAIL);//將entry push到new_node中
        new_node->count++;  //更新entry計數器
        quicklistNodeUpdateSz(new_node);        //更新ziplist的大小sz
        __quicklistInsertNode(quicklist, node, new_node, after);    //將new_node插入進去
        _quicklistMergeNodes(quicklist, node);  //左右能合併的合併
    }

    quicklist->count++;     //更新總的entry計數器
}

3.2 push操作

push一個entry到quicklist**頭節點或尾節點中ziplist的頭部或尾部**。底層調用了ziplistPush操作。

/* Add new entry to head node of quicklist.
 *
 * Returns 0 if used existing head.
 * Returns 1 if new head created. */
//push一個entry節點到quicklist的頭部
//返回0表示不改變頭節點指針,返回1表示節點插入在頭部,改變了頭結點指針
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_head = quicklist->head; //備份頭結點地址

    //如果ziplist可以插入entry節點
    if (likely(
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        quicklist->head->zl =
            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);  //將節點push到頭部
        quicklistNodeUpdateSz(quicklist->head); //更新quicklistNode記錄ziplist大小的sz
    } else {        //如果不能插入entry節點到ziplist
        quicklistNode *node = quicklistCreateNode();    //新創建一個quicklistNode節點

        //將entry節點push到新創建的quicklistNode節點中
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);

        quicklistNodeUpdateSz(node);    //更新ziplist的大小sz
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);   //將新創建的節點插入到頭節點前
    }
    quicklist->count++;                     //更新quicklistNode計數器
    quicklist->head->count++;               //更新entry計數器
    return (orig_head != quicklist->head);  //如果改變頭節點指針則返回1,否則返回0
}

/* Add new entry to tail node of quicklist.
 *
 * Returns 0 if used existing tail.
 * Returns 1 if new tail created. */
//push一個entry節點到quicklist的尾節點中,如果不能push則新創建一個quicklistNode節點
//返回0表示不改變尾節點指針,返回1表示節點插入在尾部,改變了尾結點指針
int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_tail = quicklist->tail;

    //如果ziplist可以插入entry節點
    if (likely(
            _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
        quicklist->tail->zl =
            ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);  //將節點push到尾部
        quicklistNodeUpdateSz(quicklist->tail); //更新quicklistNode記錄ziplist大小的sz
    } else {
        quicklistNode *node = quicklistCreateNode();        //新創建一個quicklistNode節點

        //將entry節點push到新創建的quicklistNode節點中
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);

        quicklistNodeUpdateSz(node);        //更新ziplist的大小sz
        _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);//將新創建的節點插入到尾節點後
    }
    quicklist->count++;             //更新quicklistNode計數器
    quicklist->tail->count++;       //更新entry計數器
    return (orig_tail != quicklist->tail);  //如果改變尾節點指針則返回1,否則返回0
}

3.3 pop操作

從quicklist的頭節點或尾節點的ziplist中pop出一個entry,分該entry保存的是字符串還是整數。如果字符串的話,需要傳入一個函數指針,這個函數叫_quicklistSaver(),真正的pop操作還是在這兩個函數基礎上在封裝了一次,來操作拷貝字符串的操作。

/* pop from quicklist and return result in 'data' ptr.  Value of 'data'
 * is the return value of 'saver' function pointer if the data is NOT a number.
 *
 * If the quicklist element is a long long, then the return value is returned in
 * 'sval'.
 *
 * Return value of 0 means no elements available.
 * Return value of 1 means check 'data' and 'sval' for values.
 * If 'data' is set, use 'data' and 'sz'.  Otherwise, use 'sval'. */
//從quicklist的頭節點或尾節點pop彈出出一個entry,並將value保存在傳入傳出參數
//返回0表示沒有可pop出的entry
//返回1表示pop出了entry,存在data或sval中
int quicklistPopCustom(quicklist *quicklist, int where, unsigned char **data,
                       unsigned int *sz, long long *sval,
                       void *(*saver)(unsigned char *data, unsigned int sz)) {
    unsigned char *p;
    unsigned char *vstr;
    unsigned int vlen;
    long long vlong;
    int pos = (where == QUICKLIST_HEAD) ? 0 : -1;   //位置下標

    if (quicklist->count == 0)  //entry數量爲0,彈出失敗
        return 0;

    //初始化
    if (data)
        *data = NULL;
    if (sz)
        *sz = 0;
    if (sval)
        *sval = -123456789;

    quicklistNode *node;
    //記錄quicklist的頭quicklistNode節點或尾quicklistNode節點
    if (where == QUICKLIST_HEAD && quicklist->head) {
        node = quicklist->head;
    } else if (where == QUICKLIST_TAIL && quicklist->tail) {
        node = quicklist->tail;
    } else {
        return 0;           //只能從頭或尾彈出
    }

    p = ziplistIndex(node->zl, pos);    //獲得當前pos的entry地址
    if (ziplistGet(p, &vstr, &vlen, &vlong)) {  //將entry信息讀入到參數中
        if (vstr) {     //entry中是字符串值
            if (data)
                *data = saver(vstr, vlen);  //調用特定的函數將字符串值保存到*data
            if (sz)
                *sz = vlen;                 //保存字符串長度
        } else {        //整數值
            if (data)
                *data = NULL;
            if (sval)
                *sval = vlong;  //將整數值保存在*sval中
        }
        quicklistDelIndex(quicklist, node, &p); //將該entry從ziplist中刪除
        return 1;
    }
    return 0;
}

/* Return a malloc'd copy of data passed in */
//將data內容拷貝一份並返回地址
REDIS_STATIC void *_quicklistSaver(unsigned char *data, unsigned int sz) {
    unsigned char *vstr;
    if (data) {
        vstr = zmalloc(sz);     //分配空間
        memcpy(vstr, data, sz); //拷貝
        return vstr;
    }
    return NULL;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章