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;
}