Redis源碼閱讀【7-quicklist】

Redis源碼閱讀【1-簡單動態字符串】

1、介紹

quicklist是Redis底層最重要的數據結構之一,它是Redis對外提供的6種基本數據結構中List的底層實現。在quicklist之前,Redis採用壓縮鏈表(ziplist)和雙向鏈表(adlist)作爲List的底層實現。當元素個數較少並且元素長度比較小時,Redis採用ziplist作爲其底層存儲,當元素長度較多時候,Redis採用adlist作爲底層存儲結構,因爲修改元素的時候ziplist需要重新分配空間,這會導致效率降低。

2、quicklist

quicklist由List 和 ziplist 組合而成,ziplist在前面的文章種已經介紹了。本章將會基於ziplist繼續介紹quicklist。

2.1、List

鏈表是一種線性結構,其中各對象按線性順序排列。鏈表與數組不同點在於,數組的順序由下標決定,鏈表的順序由對象的指針決定。List是鏈型數據存儲常用的數據結構,可以是單鏈表,雙鏈表,循環鏈表非循環鏈表,有序鏈表,無序鏈表。鏈表相對於數組具有可以快速增加,修改的(需要重新分配內存空間),刪除的優點。但是由於鏈表的查詢效率是O(n),所以並不適用於快速查找的場景,Redis 3.2之前使用的是雙向非循環鏈表,結構如下圖所示:
在這裏插入圖片描述
註明:函數指針指向的是一些方法函數用於操作鏈表

2.2、quicklist

quicklist是Redis3.2中引入的新結構,能夠在時間效率和空間效率間實現較好的折中。quicklist是一個雙向鏈表,鏈表中的每個節點都是一個ziplist結構,quicklist可以看成是將雙向鏈表將若干個小型的ziplist組合在一起的數據結構。當ziplist節點個數較多的時候,quicklist退化成雙向鏈表,一個極端的情況就是每個ziplist節點只有一個entry,即只有一個元素。當ziplist元素較少的時候,quicklist可以退化成ziplist,另一種極端的情況就是,整個quicklist中只有一個ziplist節點。結構如下圖:
在這裏插入圖片描述
代碼定義如下(quicklist.h):

typedef struct quicklist {
    quicklistNode *head; //頭結點
    quicklistNode *tail; //尾節點
    unsigned long count; //quicklist中的元素總數      
    unsigned long len;   //quicklistNode節點個數       
    int fill : 16;       //每個quicklistNode中的ziplist長度(或者指定大小類型)   
    unsigned int compress : 16; //不壓縮的末端節點深度 
} quicklist;

其中 head、tail 指向 quicklist 的首尾節點,count 爲 quicklist 中元素總數;len 爲 quicklistNode 的節點個數,fill用來指明每個quicklistNode中ziplist長度,當fill爲正數時,表明每個ziplist最多含有的數據項數,當fill爲負數的時含義如下:

數值 含義
-1 ziplist節點最大爲4KB
-2 ziplist節點最大爲8KB
-3 ziplist節點最大爲16KB
-4 ziplist節點最大爲32KB
-5 ziplist節點最大爲64KB

從表中可以看出來,fill取值爲負數的時候,必須大於等於-5。我們可以通過Redis配置修改參數list-max-ziplist-size來配置節點佔用內存大小。實際上的ziplist節點佔用的空間會在這個基礎上上下浮動,考慮quicklistNode節點個數較多的時候,我們經常訪問的是兩端的數據,爲了進一步節省空間,Redis運行對中間的quicklistNode節點進行壓縮,通過修改參數list-compress-depth進行配置,即設置compress參數的大小,爲了更好的理解compress的含義,下面給出,當compress爲1時,quicklistNode個數爲3時的結構示意圖:
在這裏插入圖片描述

2.2.1、quicklistNode

quicklistNode是一個quicklist中的節點,其結構如下 (quicklist.h)

typedef struct quicklistNode {
    struct quicklistNode *prev; //前驅
    struct quicklistNode *next; //後繼
    unsigned char *zl;          //指向元素的指針
    unsigned int sz;            //整個ziplist的字節大小 
    unsigned int count : 16;    //ziplist的元素數量 
    unsigned int encoding : 2;  //編碼方式:1原生編碼 2使用LZF壓縮  
    unsigned int container : 2; //zl指向的容器類型 1表示none 2表示使用ziplist存儲 
    unsigned int recompress : 1;//代表這個節點之前是否是壓縮節點,若是,則在使用壓縮節點前先進行解壓,使用後需要重新壓縮,此外爲1,代表是壓縮節點; 
    unsigned int attempted_compress : 1; //attempted_compress測試時使用; 
    unsigned int extra : 10; //預留字段 
} quicklistNode;

2.2.2、quicklistLZF

此外,當我們對ziplist利用LZF算法進行壓縮的時候,quicklistNode節點指向的結構爲quicklistLZFquicklistLZF結構如上面的圖片所示,其中sz表示compressed所佔字節大小。結構如下:

typedef struct quicklistLZF {
    unsigned int sz;   //壓縮後節點所佔字節大小 
    char compressed[]; //壓縮數據
} quicklistLZF;

2.2.2、quicklistEntry

當我們使用quicklistNode中ziplist中的一個節點時候,Redis提供了quicklistEntry結構以便於使用,該結構如下:

typedef struct quicklistEntry {
    const quicklist *quicklist; //quicklist
    quicklistNode *node;        //指向當前元素所在的quicklistNode
    unsigned char *zi;          //指向當前元素所在的ziplist
    unsigned char *value;       //指向該節點的字符串內容
    long long longval;          //爲該節點的整數值
    unsigned int sz;            //該節點的大小
    int offset;                 //表明該節點相對於整個ziplist的偏移量,即該節點是ziplist的第多少個entry
} quicklistEntry;

2.2.3、quicklistIter

quicklistIter是quicklist中用於遍歷的迭代器,結構如下:

typedef struct quicklistIter {
    const quicklist *quicklist; //quicklist
    quicklistNode *current;     //指向當前遍歷到的元素
    unsigned char *zi;          //指向元素所在的ziplist
    long offset;                //offset表明節點在所在的ziplist中的偏移量
    int direction;              //direction表明迭代器的方向
} quicklistIter;



//通過迭代器遍歷 quicklist並將結果放入 quicklistEntry
int quicklistNext(quicklistIter *iter, quicklistEntry *entry) {
    //初始化結構體 quicklistEntry
    initEntry(entry);

    if (!iter) {
        D("Returning because no iter!");
        return 0;
    }
    entry->quicklist = iter->quicklist;
    entry->node = iter->current;
    if (!iter->current) {
        D("Returning because current node is NULL") 
        return 0;
    }
    unsigned char *(*nextFn)(unsigned char *, unsigned char *) = NULL;
    int offset_update = 0;

    if (!iter->zi) {     
        quicklistDecompressNodeForUse(iter->current);
        iter->zi = ziplistIndex(iter->current->zl, iter->offset);
    } else {
        //剛剛開始遍歷需要解碼元素,並獲取 zi      
        if (iter->direction == AL_START_HEAD) { //正向遍歷
            nextFn = ziplistNext; //壓縮列表函數指針,用於獲取下一個元素
            offset_update = 1;
        } else if (iter->direction == AL_START_TAIL) { //逆向遍歷
            nextFn = ziplistPrev; //壓縮列表函數指針,用於獲取上一個元素
            offset_update = -1;
        }
        iter->zi = nextFn(iter->current->zl, iter->zi);
        iter->offset += offset_update;
    }

    entry->zi = iter->zi;
    entry->offset = iter->offset;

    // if(iter->zi) 的主要目的是,判斷是不是第一次遍歷,還沒解碼,如果是先解碼再解析
    if (iter->zi) {
        /* Populate value from existing ziplist position */
        //解碼將元素放入 entry
        ziplistGet(entry->zi, &entry->value, &entry->sz, &entry->longval);
        return 1;
    } else {
        quicklistCompress(iter->quicklist, iter->current);
        if (iter->direction == AL_START_HEAD) {//正向遍歷          
            D("Jumping to start of next node");
            iter->current = iter->current->next;
            iter->offset = 0;
        } else if (iter->direction == AL_START_TAIL) { //逆向遍歷          
            D("Jumping to end of previous node");//日誌打印
            iter->current = iter->current->prev;
            iter->offset = -1;
        }
        iter->zi = NULL;
        return quicklistNext(iter, entry);
    }
}

3、數據壓縮

quicklist每個節點的實際數據存儲結構爲ziplist,這種結構的主要優勢在於節省內存空間。爲了進一步降低ziplist佔用空間,Redis允許對ziplist再進行一次壓縮,Redis採用的壓縮算法是LZF,壓縮後數據可以分爲多個片段,每個片段有兩個部分,一部分是解釋字段,另一部分是存放具體的數據字段。解釋字段可以佔用1~3個字節,數據字段可能不存在。
在這裏插入圖片描述
具體而言,LZF壓縮的數據格式有三種,即解釋字段有三種。

1、字面型,解釋字段佔用1給字節,數據字段長度由解釋字段的後5位決定。如下圖所示:
在這裏插入圖片描述
2、簡短重複型,解釋字段佔用2個字節,沒有數據字段,數據內容與前面數據內容重複,重複長度小於8,示例如下圖所示:在這裏插入圖片描述
3、批量重複型,解釋字段佔3字節,沒有數據字段,數據內容與前面內容重複。如圖所示:
在這裏插入圖片描述

3.1、壓縮

LZF數據壓縮的基本思想是:數據與前面重複的,記錄重複位置以及重複長度,否則直接記錄原始數據內容。壓縮算法的流程如下:

1、遍歷輸入字符串,對當前字符及其後面2個字符進行散列運算
2、如果在Hash表中找到曾出現的記錄,則計算重複字節的長度以及位置,反之直接輸出數據

方法定義如下:

//in_data 和 in_len 爲輸入數據和長度,out_data 和 out_len 爲輸出數據和長度
lzf_compress (const void *const in_data, unsigned int in_len,
	      void *out_data, unsigned int out_len)

3.2、解壓縮

根據LZF壓縮後的數據格式,我們可以較爲容易地實現LZF的解壓縮。值得注意的是,可能存在重複數據與當前位置重疊的情況,例如在當前位置前的15個字節處,重複了20個字節,此時需要按位逐個複製。方法定義如下:

//in_data 和 in_len 爲輸入數據和長度,out_data 和 out_len 爲輸出數據和長度
lzf_decompress (const void *const in_data,  unsigned int in_len,
                void             *out_data, unsigned int out_len)

4、基本操作

quicklist是一種數據結構,所以、是必不可少的內容。由於quicklist利用ziplist結構進行實際的數據存儲,所以quicklist的大部分操作實際是利用ziplist的函數接口實現的。

4.1、初始化

初始化是構建quicklist結構的第一步,由quicklistCreate函數完成,該函數的主要功能就是初始化quicklist結構。默認初始化的quicklist結構如圖所示:
在這裏插入圖片描述
初始化代碼如下:

quicklist *quicklistCreate(void) {
    struct quicklist *quicklist;
    quicklist = zmalloc(sizeof(*quicklist));
    quicklist->head = quicklist->tail = NULL; //初始化 head 和 tail
    quicklist->len = 0; // 初始化 len
    quicklist->count = 0; // 初始化 count
    quicklist->compress = 0; // 初始化,默認: 不壓縮
    quicklist->fill = -2; //默認大小限制 8kb
    return quicklist;
}

從初始化代碼可以看出,Redis默認quicklistNode每個ziplist的大小限制是8KB,並且不對節點進行壓縮。

4.2、添加元素

quicklist提供了push作爲添加元素的操作入口,對外暴露的接口爲quicklistPush,可以在頭部或者尾部進行插入。具體的操作函數爲quicklistPushHeadquicklistPushTail,兩者的思路基本一致,所以我們主要介紹quicklistPushHead的具體實現。
quicklistPushHead的基本思路是:查看quicklist原有的head節點是否可以插入,如果可以就直接利用ziplist的接口進行插入,否則創建一個新的quicklistNode節點進行插入。函數入參爲待插入的quicklist,需要插入的數據value以及其大小sz;函數返回值代表是否新鍵了head節點,0代表沒有新鍵,1代表新鍵了head。代碼如下:

int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_head = quicklist->head;
    if (likely(//當前quicklistNode可以插入
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        quicklist->head->zl =
            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);//插入到ziplist
        quicklistNodeUpdateSz(quicklist->head);
    } else {//當前quicklistNode不能插入
        quicklistNode *node = quicklistCreateNode();
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);//插入到ziplist
        //更新ziplist的大小到sz
        quicklistNodeUpdateSz(node);
        //將新建的quicklistNode插入到quicklist結構體中
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    quicklist->count++;
    quicklist->head->count++;
    return (orig_head != quicklist->head);
}

_quicklistNodeAllowInsert 用來判斷 quicklist 的某個quicklistNode是否可以繼續插入。_quicklistInsertNodeBefore 用於在 quicklist 的某個節點之前插入quicklistNode。如果當前ziplist已經包含節點,在ziplist插入可能會導致連鎖更新。

對於quicklist的一般插入可以分爲能繼續插入和不能繼續插入:

1、當前插入的位置所在的quicklistNode仍然可以繼續插入,此時可以直接插入
2、當前插入位置所在的quicklistNode不能繼續插入,此時可以分爲以下幾個情況:
(1)、需要向當前quicklistNode第一個位置插入時不能繼續插入,但是當前 ziplist 的前一個quicklistNode可以繼續插入,則將數據插入前一個quicklistNode,否則創建一個新的quicklistNode並插入。
(2)、需要向當前quicklistNode的最後一個元素插入,當前 ziplist 所在的quicklistNode的後一個quicklistNode可以插入,則將數據插入到後一個quicklistNode。如果後一個也不能插入,則新建一個quicklistNode,插入到當前quicklistNode的後面。
(3)、不滿足前面兩種情況的,則將當前待插入quicklistNode的位置爲基準,拆分成左右兩個quicklistNode。拆分的方法是:先複製一份ziplist,通過對新舊兩個ziplist進行區域刪除完成。具體參考ziplist的基本操作。

如下圖所示:
情況一:
在這裏插入圖片描述
情況二:
在這裏插入圖片描述
情況三:
在這裏插入圖片描述

4.3、刪除元素

quicklist 對於元素刪除提供了 單一元素刪除區域元素刪除 兩種方案。對於單一元素刪除,我們可以使用 quicklist 提供的quicklistDelEntry實現,也可以通過quicklistPop將頭部元素或者尾部元素彈出。quicklistDelEntry函數調用底層quicklistDelIndex函數,該函數可以刪除quicklistNode指向的 ziplist 中的某個元素,其中指針p指向 ziplist 中某個entry的起始位置。quicklistPop可以彈出頭部或者尾部元素,具體實現是通過ziplist的接口獲取元素值,再通過上述的quicklistDelIndex將數據刪除。函數定義如下:

//指定位置刪除
int quicklistDelIndex(quicklist *quicklist, quicklistNode *node,
                                   unsigned char **p);

//元素pop                                   
int quicklistPop(quicklist *quicklist, int where, unsigned char **data,
                 unsigned int *sz, long long *slong);

對於刪除區間元素,quicklist提供了quicklistDelRange接口,該函數可以從指定位置刪除指定數量的元素。函數定義如下:

int quicklistDelRange(quicklist *quicklist, const long start, const long count);

區間刪除,不只會刪除當前quicklistNode的元素,也能刪除從當前quicklistNode開始往後quicklistNode的元素

核心代碼如下:

 //extent是需要刪除的元素個數
    while (extent) {
        quicklistNode *next = node->next;
        
        unsigned long del;
        int delete_entire_node = 0;
        if (entry.offset == 0 && extent >= node->count) {
            //情況一、需要刪除整個quicklistNode
            delete_entire_node = 1;
            del = node->count;
        } else if (entry.offset >= 0 && extent >= node->count) {
            //情況二、刪除本節點剩餘所有元素
            del = node->count - entry.offset;
        } else if (entry.offset < 0) {        
            //entry.offset < 0 代表從後向前,相反數代表這個ziplist後面剩餘元素個數。
            del = -entry.offset;         
            if (del > extent)
                del = extent;
        } else {        
            //刪除本quicklistNode的部分元素
            del = extent;
        }
        //打印日誌
        D("[%ld]: asking to del: %ld because offset: %d; (ENTIRE NODE: %d), "
          "node count: %u",
          extent, del, entry.offset, delete_entire_node, node->count);       
        //如果需要整個quicklistNode刪除,則直接刪除,否則按照情況來刪除
        if (delete_entire_node) {
            __quicklistDelNode(quicklist, node);
        } else {
            quicklistDecompressNodeForUse(node);
            //範圍刪除 當前offset 開始 到需要刪除的位置
            node->zl = ziplistDeleteRange(node->zl, entry.offset, del);
            quicklistNodeUpdateSz(node);
            node->count -= del;
            quicklist->count -= del;
            quicklistDeleteIfEmpty(quicklist, node);
            if (node)
                quicklistRecompressOnly(quicklist, node);
        }

        extent -= del; //剩餘待刪除元素個數
        node = next;  //下個quicklistNode
        entry.offset = 0; //從下個quicklistNode起始位置開始刪
    }
    return 1;

4.4、更改元素

quicklist 更改元素是基於index,主要的處理函數爲quicklistReplaceAtIndex。其基本思路是先刪除原有元素,之後插入新的元素。quicklist 不適合直接改變原有的元素,主要是由於其內部結構是ziplist (前面有提到過,壓縮列表不適合更改元素) 限制的。代碼如下所示:

int quicklistReplaceAtIndex(quicklist *quicklist, long index, void *data,
                            int sz) {
    quicklistEntry entry;
    if (likely(quicklistIndex(quicklist, index, &entry))) {
        entry.node->zl = ziplistDelete(entry.node->zl, &entry.zi);
        entry.node->zl = ziplistInsert(entry.node->zl, entry.zi, data, sz);
        quicklistNodeUpdateSz(entry.node);
        quicklistCompress(quicklist, entry.node);
        return 1;
    } else {
        return 0;
    }
}

4.5、查找元素

quicklist是通過index去查找元素的,實現函數是quicklistIndex,其基本思路是:

1、定位到目標元素所在的quicklistNode節點
2、調用 ziplist 的查找接口ziplistGet得到相應的index

代碼如下:

int quicklistIndex(const quicklist *quicklist, const long long idx,
                   quicklistEntry *entry) {
    quicklistNode *n;
    unsigned long long accum = 0;
    unsigned long long index;
    //當idx值爲負數的時候,代表是從尾部向頭部的便宜量,-1代表尾部元素,確定查找方向
    int forward = idx < 0 ? 0 : 1; /* < 0 -> reverse, 0+ -> forward */
    //初始化 quicklistEntry 最終查找到的數據會放入quicklistEntry裏面
    initEntry(entry);
    entry->quicklist = quicklist;
    //判斷頭查找還是尾查找
    if (!forward) {
        index = (-idx) - 1;
        n = quicklist->tail;
    } else {
        index = idx;
        n = quicklist->head;
    }

    if (index >= quicklist->count)
        return 0;
    
    //遍歷quicklistNode節點,找到index對應的quicklistNode
    while (likely(n)) {
        if ((accum + n->count) > index) {
            break;
        } else {
            //打印日誌
            D("Skipping over (%p) %u at accum %lld", (void *)n, n->count,
              accum);
            accum += n->count;
            n = forward ? n->next : n->prev;
        }
    }

    if (!n)
        return 0;
    //打印日誌
    D("Found node: %p at accum %llu, idx %llu, sub+ %llu, sub- %llu", (void *)n,
      accum, index, index - accum, (-index) - 1 + accum);
      
    //計算index所在的ziplist的偏移量
    entry->node = n;
    if (forward) {
        entry->offset = index - accum;
    } else {
        entry->offset = (-index) - 1 + accum;
    }

    quicklistDecompressNodeForUse(entry->node);
    entry->zi = ziplistIndex(entry->node->zl, entry->offset);
    //通過ziplist獲取元素存入 quicklistEntry
    ziplistGet(entry->zi, &entry->value, &entry->sz, &entry->longval);
    return 1;
}

對於 quicklist 的迭代器主要的實現函數如下:

//獲取指向頭部,依次向後的迭代器;或指向尾部,依次向前的迭代器
quicklistIter *quicklistGetIterator(const quicklist *quicklist, int direction);
//獲取idx位置的迭代器,可以向前或者向後遍歷
quicklistIter *quicklistGetIteratorAtIdx(const quicklist *quicklist,
                                         int direction, const long long idx);
//獲取迭代器指向的下一個元素   
int quicklistNext(quicklistIter *iter, quicklistEntry *node);

5、常用API

函數名稱 函數用途 時間複雜度
quicklistCreate 創建默認quicklist O(1)
quicklistNew 創建自定義屬性quicklist O(1)
quicklistPushHead 在頭部插入數據 O(m)
quicklistPushTail 在尾部插入數據 O(m)
quicklistPush 在頭部或者尾部插入數據 O(m)
quicklistInsertAfter 在某個元素後面插入數據 O(m)
quicklistInsertBefore 在某個元素前面插入數據 O(m)
quicklistDelEntry 刪除某個元素 O(m)
quicklistDelRange 刪除某個區間的所有元素 O(1/m+ m)
quicklistPop 彈出頭部或者尾部元素 O(m)
quicklistReplaceAtIndex 替換某個元素 O(m)
quicklistIndex 獲取某個位置的元素 O(n+m)
quicklistGetIterator 獲取指向頭部或尾部的迭代器 O(1)
quicklistGetIteratorAtIdx 獲取特定位置的迭代器 O(n+m)
quicklistNext 獲取迭代器下一個元素 O(m)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章