這裏直接貼ziplist.c
中的作者的註釋,從註釋中我們可以直觀的看到ziplist
是用於存儲string字符串的雙向鏈表,其目的是更好地利用內存去存儲數據,減少無用數據的比例。
/* The ziplist is a specially encoded dually linked list that is designed
* to be very memory efficient. It stores both strings and integer values,
* where integers are encoded as actual integers instead of a series of
* characters. It allows push and pop operations on either side of the list
* in O(1) time. However, because every operation requires a reallocation of
* the memory used by the ziplist, the actual complexity is related to the
* amount of memory used by the ziplist.
*
* ziplist是一個特殊編碼的的雙鏈表,他被設計成能夠高效利用內存。它能夠存儲字符串和整數,其中
* 整數被編碼爲真正的數字而不是一連串的字符。他允許在O(1)時間內對齊從兩邊進行push、pop操作。
* 然而,因爲每個操作需要內存的重新分配,這個真實的複雜度依賴於ziplist總計使用的內存數。
* ----------------------------------------------------------------------------
*
* ZIPLIST OVERALL LAYOUT:
* The general layout of the ziplist is as follows:
* <zlbytes><zltail><zllen><entry><entry><zlend>
*
* <zlbytes> is an unsigned integer to hold the number of bytes that the
* ziplist occupies. This value needs to be stored to be able to resize the
* entire structure without the need to traverse it first.
* <zlbytes> 是一個存儲ziplist佔用字節數量的無符號整數。這個值需要存儲,
* 從而當想要resize整個結構時,我們不必先遍歷他
*
* <zltail> is the offset to the last entry in the list. This allows a pop
* operation on the far side of the list without the need for full traversal.
* <zltail> 是距離鏈表最後一個條目的偏移量。這樣可以不用整個遍歷而對遠端進行一次pop操作。
*
* <zllen> is the number of entries.When this value is larger than 2**16-2,
* we need to traverse the entire list to know how many items it holds.
* <zllen>是條目數量。當這個值超過2^16 - 2,我們需要全部遍歷整個列表才能知道他有多少項
*
* <zlend> is a single byte special value, equal to 255, which indicates the
* end of the list.
* <zelend>是一個等於255的特殊字節,它指示鏈表的結束
*
* ZIPLIST ENTRIES:
* ZIP列表條目
* Every entry in the ziplist is prefixed by a header that contains two pieces
* of information. First, the length of the previous entry is stored to be
* able to traverse the list from back to front. Second, the encoding with an
* optional string length of the entry itself is stored.
* 每個ziplist中的條目都有一個頭部前綴,這個前綴包含兩部分信息。首先,存儲上一個
* 條目的長度使其能夠從後向前遍歷。第二,編碼和一個可選的自身存儲的條目的字符串長度。
*
* The length of the previous entry is encoded in the following way:
* If this length is smaller than 254 bytes, it will only consume a single
* byte that takes the length as value. When the length is greater than or
* equal to 254, it will consume 5 bytes. The first byte is set to 254 to
* indicate a larger value is following. The remaining 4 bytes take the
* length of the previous entry as value.
* (第一部分)
* 前一個條目的長度使用以下方式進行編碼:
* 如果這個長度小於254字節,他會僅佔用一個字節,將長度設置爲其值
* 當這個長度超過或者等於254時,他會佔用5個字節。
* 第一個字節會被設置爲254去指明其後跟隨了一個更大的數字。剩餘的4字節將上一個字節的長度作爲其值。
*
* The other header field of the entry itself depends on the contents of the
* entry. When the entry is a string, the first 2 bits of this header will hold
* the type of encoding used to store the length of the string, followed by the
* actual length of the string. When the entry is an integer the first 2 bits
* are both set to 1. The following 2 bits are used to specify what kind of
* integer will be stored after this header. An overview of the different
* types and encodings is as follows:
*
* (第二部分)
* 其餘的頭部領域依賴於條目內容。當這個條目是一個string時,頭部的前兩個bits
* 會保存存儲字符串長度的編碼類型,其後跟隨的爲字符串的實際長度。
* 當這個條目是一個整數時,前兩個bit均會設置爲1.接下來的兩個bit會被用於指明什麼類型的整數
* 會被存儲。
* 一個不同的類型和編碼概述如下:
* |00pppppp| - 1 byte
* String value with length less than or equal to 63 bytes (6 bits).
* 只使用6bit去指明字符串的長度
* |01pppppp|qqqqqqqq| - 2 bytes
* String value with length less than or equal to 16383 bytes (14 bits).
* 使用14bits去存儲字符串長度,最高到達16383長度
* |10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
* String value with length greater than or equal to 16384 bytes.
* 使用4bytes去存儲這個字符串,2^32能夠表達4G的容量,所以只要大於16384bytes的
* 字符串都將使用這種方式進行操作
*
* 接下來的的條目表示整數
* |11000000| - 1 byte
* Integer encoded as int16_t (2 bytes).
* 00 指明整數編碼爲int16_t
* |11010000| - 1 byte
* Integer encoded as int32_t (4 bytes).
* 01
* |11100000| - 1 byte
* Integer encoded as int64_t (8 bytes).
* 10 整數編碼爲64_t
* |11110000| - 1 byte
* Integer encoded as 24 bit signed (3 bytes).
* 11 整數編碼爲24byte的符號整數
* |11111110| - 1 byte // 1 字節編碼數字這麼奇怪的原因在後面
* Integer encoded as 8 bit signed (1 byte).
* 111110 8bit符號整數
* |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
* Unsigned integer from 0 to 12. The encoded value is actually from
* 1 to 13 because 0000 and 1111 can not be used, so 1 should be
* subtracted from the encoded 4 bit value to obtain the right value.
* 1111xxxx 編碼的,我們使用最後的xxxx表示4bit整數,由於我們沒辦法使用1111,1111、
* 1111,1110 和 1111,0000這三個數字,所以我們只能表示1到13。我們想要0-12就要將最後結果值減1;
* |11111111| - End of ziplist.
*/
這裏我分析幾個關鍵問題,這些問題在源代碼中作者也有介紹。
其高效利用內存是如何實現的
其實高效的意思就是我們要存儲更多的有效數據,避免對無效數據的存儲。例如傳統的鏈表,如果我們僅僅存儲一個int
,結構體的定義一般爲
struct node{
int val;
struct node *next;
}
這個結構體中有效數據僅爲50%,因爲另一半數據我們必須拿去維護鏈表結構。所以對於存儲小型數據來說,鏈表結構就相對性價比較低。
除此之外還有一種浪費就是,假設我們存儲數據{123, 12, 41, 31}
,我們使用int(4 bytes)
去存儲,但是實際上我們可以僅使用char(1 bytes)
就可以完成這些數據的存儲。這將節省下將近四倍的空間。如果存儲的數據中大多是這種small int
,那麼選取合適的int
是完全能夠達到降低內存使用的目標的。
ziplist
要求佔用一個連續的內存空間,每個數據存儲在每個entry
中。爲了滿足entry不定長的需求,我們需要在當前節點中記錄自身節點的大小和前一個節點的大小(實現雙向鏈表)。那麼這兩個int
我們也需要選擇合適的大小去存儲,否則我們還是會陷入爲了維護結構而佔用過多存儲比例的結構。(數組就是一個存儲率100%的數據結構,應爲他的結構是通過其內存位置關係來維護,沒有代價)。
通過時間換取空間,其操作更爲複雜
由於我們對於每個entry都進行了編碼操作,例如我們查看prelen
,我們需要先去判斷他是採用了5 bytes
存儲還是1bytes
存儲,如果採用了1bytes
存儲,那麼其值爲p[0]
否則爲p[1:4]
。
並且我們如果想要獲取len
,在未獲取prelen
的具體存儲情況下根本無法獲得,因爲我們無法得知len
的起始位置。
由於ziplist
要求必須存儲在一個連續的內存空間當中,所以當我們對其進行插入或者刪除時,我們必須對其進行zrealloc
和memcpy
。並且其操作還需要像鏈表那樣只能挨個尋找,無法實現隨即查詢。
結構的波動問題
我們假設存儲的每個entry
爲253 bytes。這樣的entrys
一共有100多個。那麼我們來看一個問題:
如果此時我們在頭部插入一個500 bytes的entry
。當元素插入成功後,我們需要修改next entry
的prelen
,此時我們可以看到原先的1 bytes prelen
無法存儲下500,那麼我們需要將其誇大至5 bytes
(既然面對擴容,我們就必須調用zrealloc
和memcpy
)。然而這還沒完,當我們對next entry
進行擴容完畢時,next next entry
的prelen
對應值將不再是253
而應是257
,我們還需要對next next entry
進行擴容以讓其prelen
能夠容納下257
,以此類推我們將會執行100次zrealloc
和memcpy
,而這一切都是爲了完成一次插入。
作者在源代碼中是這樣描述這個問題的
/* When an entry is inserted, we need to set the prevlen field of the next
* entry to equal the length of the inserted entry. It can occur that this
* length cannot be encoded in 1 byte and the next entry needs to be grow
* a bit larger to hold the 5-byte encoded prevlen. This can be done for free,
* because this only happens when an entry is already being inserted (which
* causes a realloc and memmove). However, encoding the prevlen may require
* that this entry is grown as well. This effect may cascade throughout
* the ziplist when there are consecutive entries with a size close to
* ZIP_BIGLEN, so we need to check that the prevlen can be encoded in every
* consecutive entry.
*
* Note that this effect can also happen in reverse, where the bytes required
* to encode the prevlen field can shrink. This effect is deliberately ignored,
* because it can cause a "flapping" effect where a chain prevlen fields is
* first grown and then shrunk again after consecutive inserts. Rather, the
* field is allowed to stay larger than necessary, because a large prevlen
* field implies the ziplist is holding large entries anyway.
*
* The pointer "p" points to the first entry that does NOT need to be
* updated, i.e. consecutive fields MAY need an update. */
/* 當一個entry被插入時,我們需要將next entry的prevlen設置爲插入的entry。
* 這可能會導致當前entry的長度無法被編碼爲1bytes長度,而next entry需要增長從而存儲5 bytes編碼的prelen
* 這個可以不用擔心,因爲這個僅發生在entry已經被插入時(插入時將會導致realloc 和 memmove)。
* 然而,編碼這個prevlen可能需要此entry同樣增長。
* 當有一連串entries其大小都相近於ZIP_BIGLEN,這個影響可能造成壞的影響對於整個ziplist。
* 所以我們需要去檢查每個prelen是否都能編碼進entry
*
* 注意需要編碼prevlen的bytes可以收縮,這個影響會造成相反的效果。
* 這個影響我們故意忽略,因爲他會造成一種波動的影響。當一條鏈發生增長之後可能會由於新的插入導致收縮。
* 我們寧願這個區域的大小超過實際需求,因爲大的prelen暗指ziplist中存儲着大的entries。
*/
static 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) {
// cur 爲 當前entry的結構體
cur = zipEntry(p);
rawlen = cur.headersize + cur.len;
rawlensize = zipPrevEncodeLength(NULL,rawlen);
/* Abort if there is no next entry. */
if (p[rawlen] == ZIP_END) break;
// next 爲下一個entry的結構體
next = zipEntry(p+rawlen);
/* Abort when "prevlen" has not changed. */
// 當prevlen未發生改變時,我們跳出循環
if (next.prevrawlen == rawlen) break;
// 當prevlen發生改變時,我們需要修改他
if (next.prevrawlensize < rawlensize) {
/* The "prevlen" field of "next" needs more bytes to hold
* the raw length of "cur". */
/* next的 prevlen 需要更多的bytes 去存儲 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);
zipPrevEncodeLength(np,rawlen);
/* Advance the cursor */
/* 進一步遞歸循環,保證整個ziplist正確性 */
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. */
// 這個將導致收縮,但是我們想要避免這樣。所以我們直接設置rawlen 進入相應bytes中
zipPrevEncodeLengthForceLarge(p+rawlen,rawlen);
} else {
// 剛好容納地下,我們此時只用改一下大小就可以
zipPrevEncodeLength(p+rawlen,rawlen);
}
/* Stop here, as the raw length of "next" has not changed. */
break;
}
}
return zl;
}