版本:redis - 5.0.4
參考資料:redis設計與實現
文件:src下的ziplist.c ziplist.h
一、基礎知識
壓縮列表是Redis爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序性數據結構。一個壓縮列表可以包含任意多個節點,每個節點可以保存一個字節數組或者一個整數值(短字符串或小的整數)。
1、壓縮列表的各個組成部分及詳細說明
ziplist是由以下幾部分組成:
zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend |
---|
屬性 | 類型 | 長度 | 用途 |
---|---|---|---|
zlbytes | uint32_t | 4字節 | 記錄整個壓縮列表佔用的內存字節數:在對壓縮列表進行內存重分配,或者計算zlend的位置時使用 |
zltail | uint32_t | 4字節 | 記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少個字節:通過這個偏移量,程序無需遍歷整個壓縮列表就可以確定表尾節點的地址 |
zllen | uint16_t | 2字節 | 記錄了壓縮列表包含的節點數量 |
entryX | 列表節點 | 不定 | 壓縮列表包含的各個節點,節點的長度由節點保存的內存決定 |
zlend | uint8_t | 1字節 | 特殊值oxFF(十進制255),用於標記壓縮列表的末尾 |
2、列表節點
typedef struct zlentry {
unsigned int prevrawlensize;//前一節點encoding的長度
unsigned int prevrawlen;//前一個節點的長度
unsigned int lensize;//當前節點encoding的長度(byte)
unsigned int len;//當前節點長度,使用了多少byte
unsigned int headersize;//prevrawlensize + lensize
unsigned char encoding; /* Set to ZIP_STR_* or ZIP_INT_* depending on
the entry encoding. However for 4 bits
immediate integers this can assume a range
of values and must be range-checked. */
unsigned char *p;//指向每一個節點的開始,也就是prevrawlen的位置
} zlentry;
3、encoding
ziplist節約內存很重要的方式就是encoding,我們要弄清楚encoding是什麼:
encoding:記錄了節點所保存數據的類型以及長度。
- 一字節、兩字節或者五字節長,值最高位爲00、01、或者10是字節數組編碼:這種編碼表示節點保存着字節數組,數組長度由編碼去掉最高兩位的其他位記錄。
- 一字節長,值的最高位爲11的是整數編碼:表示保存的是整數值,整數值類型和長度由編碼去除最高兩位的其他位記錄。
字節數組編碼
編碼 | 編碼長度 | 保存的值 |
---|---|---|
00bbbbbb | 1 字節 | 長度小於等於 63 字節的字節數組 |
01bbbbbb xxxxxxxx | 2 字節 | 長度小於等於 16383 字節的字節數組 |
10______ aaaaaaaa bbbbbbbb cccccccc dddddddd | 5 字節 | 長度小於等於 4294967295 的字節數組 |
整數編碼
編碼 | 編碼長度 | 保存的值 |
---|---|---|
11000000 | 1 字節 | int16_t 類型的整數 |
11010000 | 1 字節 | int32_t 類型的整數 |
11100000 | 1 字節 | int64_t 類型的整數 |
11110000 | 1 字節 | 24 位有符號整數 |
11111110 | 1 字節 | 8 位有符號整數 |
1111xxxx | 1 字節 | 無, 因爲編碼本身的 xxxx 四個位已經保存了一個介於 0 和12 之間的值 |
二、連鎖更新
前提:
每個節點都存儲了前一個節點的長度;
根據節點長度不同選擇不同字節大小的空來存儲長度信息(節省空間);
情景:假設有一個ziplist,存儲了幾個長度在250到253字節(next的prelen只需要1字節)的節點(只有這幾個節點)。現在插入一個大於等於254字節(next的prelen需要5字節)大小的新節點在ziplist頭部。
結果:
node1的prelen只有一個字節大小,不夠,需要重新申請空間,變成五個字節。申請之後,node1大小超過254,也需要它的下一個節點也需要五個字節來存儲它的長度,即node2。
同理,node2申請空間,擴大,則node3也需要,之後node4,node5都需要。
我們只是插入了一個新節點,但是整個list都改變了,每個節點都要重新申請空間。這種連續的多次空間擴展稱之爲連鎖更新。
/*
插入節點時, 我們需要將下一個節點的前一節點長度字段設置值爲插入節點的長度。
可能會發生這種情況:下一個節點的prelen字段需要增長, 1字節->5字節。
這隻發生在有節點插入的情況下 (會導致 realloc 和 memmove)。
當有連續節點大小接近zip _ big _ prvlen時, 這種效果可能會在ziplist中級聯,
注意:這種效果也可能發生在反向, 其中prevlen字段所需的字節可能會縮小。
鏈式的節點 先增長,再縮小會導致頻繁的空間resize,因此prevlen字段縮小的情況被故意忽略,
字段長度允許保持大於必要的字節,。
指針p 指向不需要插入後的第一個節點。
*/
unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;
size_t offset, noffset, extra;
unsigned char *np;
zlentry cur, next;
//鏈表不爲空
while (p[0] != ZIP_END) {
//獲取p所指向的節點的全部信息,存在cur指向的空間中
zipEntry(p, &cur);
rawlen = cur.headersize + cur.len;
rawlensize = zipStorePrevEntryLength(NULL,rawlen);
//沒有下一個節點,結束
if (p[rawlen] == ZIP_END) break;
//有下一個節點,取得信息,放在next中
zipEntry(p+rawlen, &next);
//判斷next的prevrawlen是否被更改:沒有(不發生更新),結束
if (next.prevrawlen == rawlen) break;
//prevrawlen字段需要擴展
if (next.prevrawlensize < rawlensize) {
/* The "prevlen" field of "next" needs more bytes to hold
* the raw length of "cur". */
offset = p-zl;
extra = rawlensize-next.prevrawlensize;
zl = ziplistResize(zl,curlen+extra);
p = zl+offset;
/* Current pointer and offset for next element. */
np = p+rawlen;
noffset = np-zl;
/* Update tail offset when next element is not the tail element. */
if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
}
/* Move the tail to the back. */
memmove(np+rawlensize,
np+next.prevrawlensize,
curlen-noffset-next.prevrawlensize-1);
zipStorePrevEntryLength(np,rawlen);
/* Advance the cursor */
p += rawlen;
curlen += extra;
} else {//需要緊縮
if (next.prevrawlensize > rawlensize) {
/* This would result in shrinking, which we want to avoid.
* So, set "rawlen" in the available bytes. */
zipStorePrevEntryLengthLarge(p+rawlen,rawlen);
} else {
zipStorePrevEntryLength(p+rawlen,rawlen);
}
/* Stop here, as the raw length of "next" has not changed. */
break;
}
}
return zl;
}
/*
指針p指向前一個節點,len爲當前長度
如果前一個節點更改大小, 此函數返回編碼前一節點長度所需的字節數的差異。
如果需要更多的空間, 該函數返回正數;如果需要較少的空間, 則返回負數; 如果需要相同的空間, 則返回0。
*/
int zipPrevLenByteDiff(unsigned char *p, unsigned int len) {
unsigned int prevlensize;
//編碼前一節點長度所需的字節數,存在prevlensize中
ZIP_DECODE_PREVLENSIZE(p, prevlensize);
//zipStorePrevEntryLength(NULL, len):獲取編碼len長度所需的字節數
return zipStorePrevEntryLength(NULL, len) - prevlensize;
}
連鎖更新出現的要求:
連續的,多個,長度介於250到253字節的節點;
概率很低,所以連鎖更新對ziplist的影響不大
三、ziplist.h
//生成
unsigned char *ziplistNew(void);
//把 second 追加到 first, 合併這兩個
unsigned char *ziplistMerge(unsigned char **first, unsigned char **second);
//從頭或者從尾插入s
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where);
//獲取指定下標的
unsigned char *ziplistIndex(unsigned char *zl, int index);
//下一個節點
unsigned char *ziplistNext(unsigned char *zl, unsigned char *p);
//前一個節點
unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p);
//獲取
unsigned int ziplistGet(unsigned char *p, unsigned char **sval, unsigned int *slen, long long *lval);
//插入
unsigned char *ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen);
//刪除
unsigned char *ziplistDelete(unsigned char *zl, unsigned char **p);
//從index開始刪除num個
unsigned char *ziplistDeleteRange(unsigned char *zl, int index, unsigned int num);
//比較
unsigned int ziplistCompare(unsigned char *p, unsigned char *s, unsigned int slen);
//查找
unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip);
//長度
unsigned int ziplistLen(unsigned char *zl);
//原始長度
size_t ziplistBlobLen(unsigned char *zl);
//打印第一個
void ziplistRepr(unsigned char *zl);
體會:由於存儲了每個節點的字節數,無需遍歷,用指針p加減字節數,就能讓p指向自己需要的地址空間,獲得需要的值。