文章目錄
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
節點指向的結構爲quicklistLZF
。quicklistLZF
結構如上面的圖片所示,其中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
,可以在頭部或者尾部進行插入。具體的操作函數爲quicklistPushHead
與 quicklistPushTail
,兩者的思路基本一致,所以我們主要介紹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) |