壓縮列表用於存儲長度受限的字符串和整數。廢話不多說,直接上redis壓縮列表的內存結構示意圖:
從圖中可以看出,redis壓縮列表由表示壓縮列表佔總內存的字節數的zlbytes,表示到達ziplist 表尾節點的偏移量的zltail,表示ziplist 中節點的數量的zllen,各個節點以及用於標記ziplist的末端的zlend。
注意:zllen並不是一直表示節點的數量,只有zllen小於UINT16_MAX時纔是,當這個值等於UINT16_MAX時,節點的數量需要遍歷整個ziplist 才能計算得出。zlend的值是固定的,也就是255.
從圖中可以看出,壓縮列表總是有11個字節的固定長度(4+4+2+1),而這11個字節的長度也就是壓縮列表的頭部與尾部,在新建一個壓縮列表的時候,也就是隻有這11個字節,下面來看下新建壓縮列表的函數:
unsigned char *ziplistNew(void) {
// 分配 2 個 32 bit,一個 16 bit,以及一個 8 bit
// 分別用於 <zlbytes><zltail><zllen> 和 <zlend>
unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
unsigned char *zl = zmalloc(bytes);
// 設置長度
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
// 設置表尾偏移量
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
// 設置列表項數量
ZIPLIST_LENGTH(zl) = 0;
// 設置表尾標識
zl[bytes-1] = ZIP_END;
return zl;
}
而ZIPLIST_HEADER_SIZE的定義是個宏,如下:#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
結合上面的宏可以看出ziplistNew中的bytes就是11。ziplistNew的功能也很簡單就不多說了。
在講述壓縮列表插入之前,先要介紹下其餘的東西。上面的圖講述了壓縮列表的格式,但是並沒有各個節點的格式,下面以一張圖來描述下:
上圖就描述了各個節點的格式,但是節點中域的長度並不是固定的,下面來講述下:
pre_entry_length從字面意思就能看出來它表示前一個節點的長度,pre_entry_length可以佔用1個字節也可以佔用5個字節。如果前一節點的長度小於254 字節,那麼只使用一個字節保存它的值。如果前一節點的長度大於等於254 字節,那麼將第1 個字節的值設爲254 ,然後用接下來的4 個字節保存實際長度。
encoding可以分爲四種,其中三種是爲字符串準備的,最後一種就是爲整數準備的。具體如下表格所示:
以上是編碼字符串所用到的。下面的是編碼整數的
下面結合插入數據到壓縮表來講述這些編碼方式。插入數據到壓縮表主要是通過ziplistInsert,這個函數會調用__ziplistInsert,而實際插入數據的也是__ziplistInsert這個函數來進行操作的。這裏我只列出比較重要的一部分代碼。
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
.....
if (zipTryEncoding(s,slen,&value,&encoding)) {
/* 'encoding' is set to the appropriate integer encoding */
// s 可以保存爲整數,那麼繼續計算保存它所需的空間
reqlen = zipIntSize(encoding);
} else {
/* 'encoding' is untouched, however zipEncodeLength will use the
* string length to figure out how to encode it. */
// 不能保存爲整數,直接使用字符串長度
reqlen = slen;
}
// 計算編碼 prevlen 所需的長度
reqlen += zipPrevEncodeLength(NULL,prevlen);
// 計算編碼 slen 所需的長度
reqlen += zipEncodeLength(NULL,encoding,slen);
// 如果添加的位置不是表尾,那麼必須確定後繼節點的 prevlen 空間
// 足以保存新節點的編碼長度
// zipPrevLenByteDiff 的返回值有三種可能:
// 1)新舊兩個節點的編碼長度相等,返回 0
// 2)新節點編碼長度 > 舊節點編碼長度,返回 5 - 1 = 4
// 3)舊節點編碼長度 > 新編碼節點長度,返回 1 - 5 = -4
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
.....
// 如果新節點不是添加到列表末端,那麼它後面就有其他節點
// 因此,我們需要移動這部分節點
if (p[0] != ZIP_END) {
/* Subtract one because of the ZIP_END bytes */
// 向右移動移原有數據,爲新節點讓出空間
// O(N)
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
/* Encode this entry's raw length in the next entry. */
// 將本節點的長度編碼至下一節點
zipPrevEncodeLength(p+reqlen,reqlen);
/* Update offset for tail */
// 更新 ziplist 的表尾偏移量
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
/* When the tail contains more than one entry, we need to take
* "nextdiff" in account as well. Otherwise, a change in the
* size of prevlen doesn't have an effect on the *tail* offset. */
// 有需要的話,將 nextdiff 也加上到 zltail 上
tail = zipEntry(p+reqlen);
if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
} else {
/* This element will be the new tail. */
// 更新 ziplist 的 zltail 屬性,現在新添加節點爲表尾節點
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
}
.....
if (nextdiff != 0) {
offset = p-zl;
// O(N^2)
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
......
}
代碼中大部分都有註釋,也比較簡單,這裏我要說的是if(nextdiff!=0)的情況,如果nextdiff不爲0,說明新插入的數據的長度與以前這個位置的數據長度不同,而next中的pre_entry_length就需要進行改變,所以要擴展或者收縮next的大小,next大小的改變也同時需要改變next下一個節點的pre_entry_length,直到整個壓縮列表全部進行更改。而上面的if語句就是做這件事的。