| 屬性 | 類型 | 長 度 | 說明 |
|---|---|---|---|
| zlbytes | uint32_t | 4字節 | 記錄壓縮列表占用內存字節數(包括本身所占用的 4 個字節)。 |
| zltail | uint32_t | 4字節 | 記錄壓縮列表尾節點距離壓縮列表的起始地址有多少個字節(通過這個值可以計算出尾節點的地址) |
| zllen | uint16_t | 2字節 | 記錄壓縮列表中包含的節點數量,當列表值超過可以存儲的最大值(65535)時,此值固定存儲 65535(即 2 的 16 次方 減 1),因此此時需要遍歷整個壓縮列表才能計算出真實節點數。 |
| entry | 節點 | - | 壓縮列表中的各個節點,長度由存儲的實際數據決定。 |
| zlend | uint8_t | 1字節 | 特殊字符 0xFF(即十進制 255),用來標記壓縮列表的末端(其他正常的節點沒有被標記為 255 的,因為 255 用來標識末尾,后面可以看到,正常節點都是標記為 254)。 |
ziplist 的 head 和 end 存的都是長度和標記,而 entry 存儲的是具體元素,這又是經過特殊的設計的一種存儲格式,每個 entry 都以包含兩段信息的元數據作為前綴,每一個 entry 的組成結構為:
prevlen> encoding> entry-data>
prevlen 屬性存儲了前一個 entry 的長度,通過此屬性能夠從后到前遍歷列表。 prevlen 屬性的長度可能是 1 字節也可能是 5 字節:
當鏈表的前一個 entry 占用字節數小于 254,此時 prevlen 只用 1 個字節進行表示。
prevlen from 0 to 253> encoding> entry>
當鏈表的前一個 entry 占用字節數大于等于 254,此時 prevlen 用 5 個字節來表示,其中第 1 個字節的值固定是 254(相當于是一個標記,代表后面跟了一個更大的值),后面 4 個字節才是真正存儲前一個 entry 的占用字節數。
0xFE 4 bytes unsigned little endian prevlen> encoding> entry>
注意:1 個字節完全你能存儲 255 的大小,之所以只取到 254 是因為 zlend 就是固定的 255,所以 255 這個數要用來判斷是否是 ziplist 的結尾。
encoding 屬性存儲了當前 entry 所保存數據的類型以及長度。encoding 長度為 1 字節,2 字節或者 5 字節長。前面我們提到,每一個 entry 中可以保存字節數組和整數,而 encoding 屬性的第 1 個字節就是用來確定當前 entry 存儲的是整數還是字節數組。當存儲整數時,第 1 個字節的前兩位總是 11,而存儲字節數組時,則可能是 00、01 和 10 三種中的一種。
當存儲整數時,第 1 個字節的前 2 位固定為 11,其他位則用來記錄整數值的類型或者整數值(下表所示的編碼中前兩位均為 11):
| 編碼 | 長度 | entry保存的數據 |
|---|---|---|
| 11000000 | 1字節 | int16_t類型整數 |
| 11010000 | 1字節 | int32_t類型整數 |
| 11100000 | 1字節 | int64_t類型整數 |
| 11110000 | 1字節 | 24位有符號整數 |
| 11111110 | 1字節 | 8位有符號整數 |
| 1111xxxx | 1字節 | xxxx 代表區間 0001-1101,存儲了一個介于 0-12 之間的整數,此時 entry-data 屬性被省略 |
注意:xxxx 四位編碼范圍是 0000-1111,但是 0000,1111 和 1110 已經被表格中前面表示的數據類型占用了,所以實際上的范圍是 0001-1101,此時能保存數據 1-13,再減去 1 之后范圍就是 0-12。至于為什么要減去 1 是從使用習慣來說 0 是一個非常常用的數據,所以才會選擇統一減去 1 來存儲一個 0-12 的區間而不是直接存儲 1-13 的區間。
當存儲字節數組時,第 1 個字節的前 2 位為 00、01 或者 10,其他位則用來記錄字節數組的長度:
| 編碼 | 長度 | entry保存的數據 |
|---|---|---|
| 00pppppp | 1字節 | 長度小于等于 63 字節(6 位)的字節數組 |
| 01pppppp qqqqqqqq | 2字節 | 長度小于等于 16383 字節(14 位)的字節數組 |
| 10000000 qqqqqqqq rrrrrrrr ssssssss tttttttt | 5字節 | 長度小于等于 2 的 32 次方減 1 (32 位)的字節數組,其中第 1 個字節的后 6 位設置為 0,暫時沒有用到,后面的 32 位(4 個字節)存儲了數據 |
entry-data 存儲的是具體數據。當存儲小整數(0-12)時,因為 encoding 就是數據本身,此時 entry-data 部分會被省略,省略了 entry-data 部分之后的 ziplist 中的 entry 結構如下:
prevlen> encoding>
壓縮列表中 entry 的數據結構定義如下(源碼 ziplist.c 文件內),當然實際存儲并沒有直接使用到這個結構定義,這個結構只是用來接收數據,所以大家了解一下就可以了:
typedef struct zlentry {
unsigned int prevrawlensize;//存儲prevrawlen所占用的字節數
unsigned int prevrawlen;//存儲上一個鏈表節點需要的字節數
unsigned int lensize;//存儲len所占用的字節數
unsigned int len;//存儲鏈表當前節點的字節數
unsigned int headersize;//當前鏈表節點的頭部大小(prevrawlensize + lensize)即非數據域的大小
unsigned char encoding;//編碼方式
unsigned char *p;//指向當前節點的起始位置(因為列表內的數據也是一個字符串對象)
} zlentry;
上面講解了大半天,可能大家都覺得枯燥無味了,也可能會覺得云里霧里,這個沒有關系,這些只要心里有個概念,用到的時候再查詢對應資料就可以了,并不需要全部記住,接下來讓我們一起通過兩個例子來體會一下 ziplist 到底是如何來組織存儲數據的。
下面就是一個壓縮列表的存儲示例,這個壓縮列表里面存儲了 2 個節點,節點中存儲的是整數 2 和 5:
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "5" end
第一組 4 個字節為 zlbytes 部分,0f 轉成二進制就是 1111 也就是 15,代表整個 ziplist 長度是 15 個字節。第二組 4 個字節 zltail 部分,0c 轉成二進制就是 1100 也就是 12,這里記錄的是壓縮列表尾節點距離起始地址有多少個字節,也就是就是說 [02 f6] 這個尾節點距離起始位置有 12 個字節。第三組 2 個字節就是記錄了當前 ziplist 中 entry 的數量,02 轉成二進制就是 10,也就是說當前 ziplist 有 2 個節點。第四組 2 個字節 [00 f3] 就是第一個 entry,00 表示 0,因為這是第 1 個節點,所以前一個節點長度為 0,f3 轉成二進制就是 11110011,剛好對應了表格中的編碼 1111xxxx,所以后面四位就是存儲了一個 0-12位的整數。0011 轉成十進制就是 3,減去 1 得到 2,所以第一個 entry 存儲的數據就是 2。第五組 2 個字節 [02 f6] 就是第二個 entry,02 即為 2,表示前一個節點的長度為 2,注意,因為這里算出來的結果是小于 254,所以就代表了這里只用到了 1 個字節來存儲上一個節點的長度(如果等于 254,這說明接下來 4 個字節才存儲的是長度),所以后面的 f6 就是當前節點的數據,轉換成二進制為 11110110,對應了表格中的編碼 1111xxxx,同樣的后四位 0110 存儲的是真實數據,計算之后得出是5。最后一組1個字節[ff]轉成二進制就是 11111111,代表這是整個 ziplist 的結尾。
假如這時候又添加了一個 Hello World 字符串到列表中,那么就會新增一個 entry,如下所示:
[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]
第一組的 1 個字節 02 轉成十進制就是 2 ,表示前一個節點(即上面示例中的 [02 f6])長度是 2。第 二組的2 個字節 0b 轉成二進制為 00001011,以 00 開頭,符合編碼 00pppppp,而除掉最開始的兩位 00,計算之后得到十進制 11,這就說明后面字節數組的長度是 11。第三組剛好是 11 個字節,對應了上面的長度,所以這里就是真正存儲了 Hello World 的字節數組。
上面提到 entry 中的 prevlen 屬性可能是 1 個字節也可能是 5 個字節,那么我們來設想這么一種場景:假設一個 ziplist 中,連續多個 entry 的長度都是一個接近但是又不到 254 的值(介于 250~253 之間),那么這時候 ziplist 中每個節點都只用了 1 個字節來存儲上一個節點的長度,假如這時候添加了一個新節點,如 entry1 ,其長度大于 254 個字節,此時 entry1 的下一個節點 entry2 的 prelen 屬性就必須要由 1 個字節變為 5 個字節,也就是需要執行空間重分配,而此時 entry2 因為增加了 4 個字節,導致長度又大于 254 個字節了,那么它的下一個節點 entry3 的 prelen 屬性也會被改變為 5 個字節。依此類推,這種產生連續多次空間重分配的現象就稱之為連鎖更新。同樣的,不僅僅是新增節點,執行刪除節點操作同樣可能會發生連鎖更新現象。
雖然 ziplist 可能會出現這種連鎖更新的場景,但是一般如果只是發生在少數幾個節點之間,那么并不會嚴重影響性能,而且這種場景發生的概率也比較低,所以實際使用時不用過于擔心。
本文主要講解了 Redis 當中的 ziplist(壓縮列表),一種用時間換取空間的數據結構,在介紹壓縮列表存儲結構的同時通過一個存儲示例來分析了 ziplist 是如何存儲數據的,最后介紹了 ziplist 中可能發生的連鎖更新問題。
到此這篇關于壓縮列表犧牲速度來節省內存,Redis是膨脹了嗎的文章就介紹到這了,更多相關Redis壓縮列表內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持腳本之家!
上一篇:Redis憑啥可以這么快