壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現之一。
當一個列表鍵只包含少量列表項,並且每個列表項要麼就是小整數值,要麼就是長度比較短的字符串,那麼Redis就會使用壓縮列表來作爲列表鍵的底層實現。
當一個哈希鍵只包含少量鍵值對,並且每個鍵值對的鍵和值要麼就是小整數值,要麼就是長度較短的字符串時,那麼Redis就會使用壓縮列表來做哈希鍵的底層實現。
7.1 壓縮列表的構成
壓縮列表是Redis爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構。一個壓縮列表可以包含任意多個節點(entry),每個節點可以保存一個字節數組或者一個整數值。
壓縮列表的各個組成部分:
示例:
0x3c(十六進制):3*16+12 = 60(十進制)
7.2 壓縮列表節點的構成
每個壓縮列表節點可以保存一個字節數組或者一個整數值。
字節數組可以是以下三種長度的其中一種:
而整數值可以是以下6種長度的其中一種:
每個壓縮列表節點都由previous_entry_length、encoding、content三個部分組成:
7.2.1 previous_entry_length
節點的previous_entry_length屬性以字節爲單位,記錄了壓縮列表中前一個節點的長度。previous_entry_length 屬性的長度可以是1字節或者5字節:
- 如果前一節點的長度小於254字節,那麼previous_entry_length屬性的長度爲1字節;
- 如果前一節點的長度大於等於254字節,那麼previous_entry_length屬性的長度爲5字節:其中屬性的第一字節會被設置爲0xFE(十進制值254),之後的4個字節則用於保存前一節點的長度。
因爲節點的previous_entry_length屬性記錄了前一節點的長度,所以程序可以通過指針運算,根據當前節點的起始地址來計算出前一節點的起始地址。(壓縮列表從表尾向表頭遍歷操作就是使用這一原理實現的)
7.2.2 encoding
節點的encoding屬性記錄了節點的content屬性所保存數據的類型以及長度:
- 一字節、兩字節或者五字節長,值的最高位爲00、01或者10的是字節數組編碼:這種編碼表示節點的content屬性保存着字節數組,數組的長度由編碼除去最高兩位之後的其他位記錄;
- 一字節長,值的最高位以11開頭的是整數編碼:這種編碼表示節點的content屬性保存着整數值,整數值的類型和長度由編碼除去最高兩位之後的其他位記錄;
所有可用的字節數組編碼:(表格中下劃線”_”表示留空,而b、x等變量代表實際的二進制數據)
所有可用的整數編碼:
7.2.3 content
節點的content屬性負責保存節點的值,節點值可以是一個字節數組或者整數,值的類型和長度由節點的encoding屬性決定。
上圖中,編碼的最高兩位00表示節點保存的是一個字節數組;編碼的後6位001011記錄了字節數組的長度11;
7.3 連鎖更新
考慮這樣一種情況:在一個壓縮列表中,有多個連續的、長度介於250字節到253字節(皆小於254字節)之間的節點e1至eN。
因爲e1至eN的所有節點的長度都小於254字節,所以記錄這些節點的長度只需要1字節長的previous_entry_length屬性。
這時,如果我們將一個長度大於等於254字節的新節點new設置爲壓縮列表的表頭節點,那麼new將成爲e1的前置節點。
e1節點的previous_entry_length屬性僅長1個字節,沒辦法保存新節點new的長度,所以程序將對壓縮列表執行空間重分配操作,並將e1節點的previous_entry_length屬性從原來的1字節擴展爲5字節長。
由於e1的長度增加,又會導致後續節點的previous_entry_length增加,如此一直擴展下去,程序需要不斷地對壓縮列表執行空間重分配操作,直到eN爲止。
Redis將這種在特殊情況下產生的連續多次空間擴展操作稱爲“連鎖更新(cascade update)”。
除此之外,刪除節點也有可能會引發連鎖更新。
考慮如果small、e1-eN都是大小介於250字節至253字節的節點,big節點的長度大於等於254字節。
因爲連鎖更新在最壞情況下需要對壓縮列表執行N次空間重分配操作,而每次空間重分配的最壞複雜度爲O(N),所以連鎖更新的最壞複雜度爲O(N2)。
注意,儘管連鎖更新的複雜度較高,但是真正造成性能問題的機率是很低的,ziplistPush等命令的平均複雜度僅爲O(N)。
注:redis 3.2以後,quicklist作爲列表鍵的實現底層實現之一,代替了壓縮列表。
7.4 壓縮列表API