Redis Ziplist 概述

OkidoGreen發表於2020-04-05

https://www.jianshu.com/p/37c49b4e3bd0

壓縮列表

壓縮列表(ziplist)是列表鍵和雜湊鍵的底層實現之一。

當一個列表鍵只包含少量列表項, 並且每個列表項要麼就是小整數值, 要麼就是長度比較短的字串, 那麼 Redis 就會使用壓縮列表來做列表鍵的底層實現。

比如說, 執行以下命令將建立一個壓縮列表實現的列表鍵:

redis> RPUSH lst 1 3 5 10086 "hello" "world"
(integer) 6

redis> OBJECT ENCODING lst
"ziplist"

因為列表鍵裡面包含的都是 1 、 3 、 5 、 10086 這樣的小整數值, 以及 "hello" 、 "world" 這樣的短字串。

另外, 當一個雜湊鍵只包含少量鍵值對, 並且每個鍵值對的鍵和值要麼就是小整數值, 要麼就是長度比較短的字串, 那麼 Redis 就會使用壓縮列表來做雜湊鍵的底層實現。

舉個例子, 執行以下命令將建立一個壓縮列表實現的雜湊鍵:

redis> HMSET profile "name" "Jack" "age" 28 "job" "Programmer"
OK

redis> OBJECT ENCODING profile
"ziplist"

因為雜湊鍵裡面包含的所有鍵和值都是小整數值或者短字串。

本章將對壓縮列表的定義以及相關操作進行詳細的介紹。

壓縮列表的構成

壓縮列表是 Redis 為了節約記憶體而開發的, 由一系列特殊編碼的連續記憶體塊組成的順序型(sequential)資料結構。

一個壓縮列表可以包含任意多個節點(entry), 每個節點可以儲存一個位元組陣列或者一個整數值。

圖 7-1 展示了壓縮列表的各個組成部分, 表 7-1 則記錄了各個組成部分的型別、長度、以及用途。

 

 

表 7-1 壓縮列表各個組成部分的詳細說明

屬性 型別 長度 用途
zlbytes uint32_t 4 位元組 記錄整個壓縮列表佔用的記憶體位元組數:在對壓縮列表進行記憶體重分配, 或者計算 zlend 的位置時使用。
zltail uint32_t 4 位元組 記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少位元組: 通過這個偏移量,程式無須遍歷整個壓縮列表就可以確定表尾節點的地址。
zllen uint16_t 2 位元組 記錄了壓縮列表包含的節點數量: 當這個屬性的值小於 UINT16_MAX (65535)時, 這個屬性的值就是壓縮列表包含節點的數量; 當這個值等於 UINT16_MAX 時, 節點的真實數量需要遍歷整個壓縮列表才能計算得出。
entryX 列表節點 不定 壓縮列表包含的各個節點,節點的長度由節點儲存的內容決定。
zlend uint8_t 1 位元組 特殊值 0xFF (十進位制 255 ),用於標記壓縮列表的末端。

圖 7-2 展示了一個壓縮列表示例:

  • 列表 zlbytes 屬性的值為 0x50 (十進位制 80), 表示壓縮列表的總長為 80 位元組。
  • 列表 zltail 屬性的值為 0x3c (十進位制 60), 這表示如果我們有一個指向壓縮列表起始地址的指標 p , 那麼只要用指標 p 加上偏移量 60 , 就可以計算出表尾節點 entry3 的地址。
  • 列表 zllen 屬性的值為 0x3 (十進位制 3), 表示壓縮列表包含三個節點。

 

圖 7-3 展示了另一個壓縮列表示例:

  • 列表 zlbytes 屬性的值為 0xd2 (十進位制 210), 表示壓縮列表的總長為 210 位元組。
  • 列表 zltail 屬性的值為 0xb3 (十進位制 179), 這表示如果我們有一個指向壓縮列表起始地址的指標 p , 那麼只要用指標 p 加上偏移量 179 , 就可以計算出表尾節點 entry5 的地址。
  • 列表 zllen 屬性的值為 0x5 (十進位制 5), 表示壓縮列表包含五個節點。

連鎖更新

前面說過, 每個節點的 previous_entry_length 屬性都記錄了前一個節點的長度:

如果前一節點的長度小於 254 位元組, 那麼 previous_entry_length 屬性需要用 1 位元組長的空間來儲存這個長度值。
如果前一節點的長度大於等於 254 位元組, 那麼 previous_entry_length 屬性需要用 5 位元組長的空間來儲存這個長度值。
現在, 考慮這樣一種情況: 在一個壓縮列表中, 有多個連續的、長度介於 250 位元組到 253 位元組之間的節點 e1 至 eN , 如圖 7-11 所示。


 

因為 e1 至 eN 的所有節點的長度都小於 254 位元組, 所以記錄這些節點的長度只需要 1 位元組長的 previous_entry_length 屬性, 換句話說, e1 至 eN 的所有節點的 previous_entry_length 屬性都是 1 位元組長的。

這時, 如果我們將一個長度大於等於 254 位元組的新節點 new 設定為壓縮列表的表頭節點, 那麼 new 將成為 e1 的前置節點, 如圖 7-12 所示。


 

因為 e1 的 previous_entry_length 屬性僅長 1 位元組, 它沒辦法儲存新節點 new 的長度, 所以程式將對壓縮列表執行空間重分配操作, 並將 e1 節點的 previous_entry_length 屬性從原來的 1 位元組長擴充套件為 5 位元組長。

現在, 麻煩的事情來了 —— e1 原本的長度介於 250 位元組至 253 位元組之間, 在為 previous_entry_length 屬性新增四個位元組的空間之後, e1 的長度就變成了介於 254 位元組至 257 位元組之間, 而這種長度使用 1 位元組長的 previous_entry_length 屬性是沒辦法儲存的。

因此, 為了讓 e2 的 previous_entry_length 屬性可以記錄下 e1 的長度, 程式需要再次對壓縮列表執行空間重分配操作, 並將 e2 節點的 previous_entry_length 屬性從原來的 1 位元組長擴充套件為 5 位元組長。

正如擴充套件 e1 引發了對 e2 的擴充套件一樣, 擴充套件 e2 也會引發對 e3 的擴充套件, 而擴充套件 e3 又會引發對 e4 的擴充套件……為了讓每個節點的 previous_entry_length 屬性都符合壓縮列表對節點的要求, 程式需要不斷地對壓縮列表執行空間重分配操作, 直到 eN 為止。

Redis 將這種在特殊情況下產生的連續多次空間擴充套件操作稱之為“連鎖更新”(cascade update), 圖 7-13 展示了這一過程。






 

除了新增新節點可能會引發連鎖更新之外, 刪除節點也可能會引發連鎖更新。

考慮圖 7-14 所示的壓縮列表, 如果 e1 至 eN 都是大小介於 250 位元組至 253 位元組的節點, big 節點的長度大於等於 254 位元組(需要 5 位元組的 previous_entry_length 來儲存), 而 small 節點的長度小於 254 位元組(只需要 1 位元組的 previous_entry_length 來儲存), 那麼當我們將 small 節點從壓縮列表中刪除之後, 為了讓 e1 的 previous_entry_length 屬性可以記錄 big 節點的長度, 程式將擴充套件 e1 的空間, 並由此引發之後的連鎖更新。

 

 

因為連鎖更新在最壞情況下需要對壓縮列表執行 N 次空間重分配操作, 而每次空間重分配的最壞複雜度為 O(N) , 所以連鎖更新的最壞複雜度為 O(N^2) 。

要注意的是, 儘管連鎖更新的複雜度較高, 但它真正造成效能問題的機率是很低的:

  • 首先, 壓縮列表裡要恰好有多個連續的、長度介於 250 位元組至 253 位元組之間的節點, 連鎖更新才有可能被引發, 在實際中, 這種情況並不多見;
  • 其次, 即使出現連鎖更新, 但只要被更新的節點數量不多, 就不會對效能造成任何影響: 比如說, 對三五個節點進行連鎖更新是絕對不會影響效能的;

因為以上原因, ziplistPush 等命令的平均複雜度僅為 O(N) , 在實際中, 我們可以放心地使用這些函式, 而不必擔心連鎖更新會影響壓縮列表的效能。

壓縮列表 API

表 7-4 列出了所有用於操作壓縮列表的 API 。


表 7-4 壓縮列表 API

函式 作用 演算法複雜度
ziplistNew 建立一個新的壓縮列表。 O(1)
ziplistPush 建立一個包含給定值的新節點, 並將這個新節點新增到壓縮列表的表頭或者表尾。 平均 O(N) ,最壞 O(N^2) 。
ziplistInsert 將包含給定值的新節點插入到給定節點之後。 平均 O(N) ,最壞 O(N^2) 。
ziplistIndex 返回壓縮列表給定索引上的節點。 O(N)
ziplistFind 在壓縮列表中查詢並返回包含了給定值的節點。 因為節點的值可能是一個位元組陣列, 所以檢查節點值和給定值是否相同的複雜度為 O(N) , 而查詢整個列表的複雜度則為 O(N^2) 。
ziplistNext 返回給定節點的下一個節點。 O(1)
ziplistPrev 返回給定節點的前一個節點。 O(1)
ziplistGet 獲取給定節點所儲存的值。 O(1)
ziplistDelete 從壓縮列表中刪除給定的節點。 平均 O(N) ,最壞 O(N^2) 。
ziplistDeleteRange 刪除壓縮列表在給定索引上的連續多個節點。 平均 O(N) ,最壞 O(N^2) 。
ziplistBlobLen 返回壓縮列表目前佔用的記憶體位元組數。 O(1)
ziplistLen 返回壓縮列表目前包含的節點數量。 節點數量小於 65535 時 O(1) , 大於 65535 時 O(N) 。

因為 ziplistPush 、 ziplistInsert 、 ziplistDelete 和 ziplistDeleteRange 四個函式都有可能會引發連鎖更新, 所以它們的最壞複雜度都是 O(N^2) 。

重點回顧

  • 壓縮列表是一種為節約記憶體而開發的順序型資料結構。
  • 壓縮列表被用作列表鍵和雜湊鍵的底層實現之一。
  • 壓縮列表可以包含多個節點,每個節點可以儲存一個位元組陣列或者整數值。
  • 新增新節點到壓縮列表, 或者從壓縮列表中刪除節點, 可能會引發連鎖更新操作, 但這種操作出現的機率並不高

 

相關文章