Redis 源碼簡潔剖析 05 - ziplist 壓縮列表

ziplist 是什麼

壓縮列表,內存緊湊的數據結構,佔用一塊連續的內存空間。一個 ziplist 可以包含多個節點(entry), 每個節點可以保存一個長度受限的字符數組(不以 \0 結尾的 char 數組)或者整數, 包括:

  • 字符數組

    • 長度小於等於 63 (2^6-1)字節的字符數組
    • 長度小於等於 16383 (12^14-1) 字節的字符數組
    • 長度小於等於 4294967295 (2^32-1)字節的字符數組
  • 整數

    • 4 位長,介於 012 之間的無符號整數
    • 1 字節長,有符號整數
    • 3 字節長,有符號整數
    • int16_t 類型整數
    • int32_t 類型整數
    • int64_t 類型整數

Redis 哪些數據結構使用了 ziplist?

  • 哈希鍵
  • 列表鍵
  • 有序集合鍵

ziplist 特點

優點

  • 節省內存

缺點

  • 不能保存過多的元素,否則訪問性能會下降
  • 不能保存過大的元素,否則容易導致內存重新分配,甚至引起連鎖更新

ziplist 數據結構

啥都不說了,都在註釋裏。

// ziplist 中的元素,是 string 或者 integer
typedef struct {
    // 如果元素是 string,slen 就表示長度
    unsigned char *sval;
    unsigned int slen;
    // 如果是 integer,sval 是 NULL,lval 就是 integer 的值
    long long lval;
} ziplistEntry;

爲了方便地取出 ziplist 的各個域以及一些指針地址, ziplist 模塊定義了以下宏:

// 取出 zlbytes 的值
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))

// 取出 zltail 的值
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

// 取出 zllen 的值
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

// 返回 ziplist header 部分的長度,總是固定的 10 字節
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

// 返回 ziplist end 部分的長度,總是固定的 1 字節
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))

// 返回到達 ziplist 第一個節點(表頭)的地址
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)

// 返回到達 ziplist 最後一個節點(表尾)的地址
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

// 返回 ziplist 的末端,也即是 zlend 之前的地址
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

ziplist 節點

typedef struct zlentry {
    // 前一個節點的長度,通過這個值,可以進行指針計算,從而跳轉到上一個節點
    unsigned int prevrawlen;
    unsigned int prevrawlensize;
    // entry 的編碼方式
    // 1. entry 是 string,可能是 1 2 5 個字節的 header
    // 2. entry 是 integer,固定爲 1 字節
    unsigned int lensize;
    // 實際 entry 的字節數
    // 1. entry 是 string,則表示 string 的長度
    // 2. entry 是 integer,則根據數值範圍,可能是 1, 2, 3, 4, 8
    unsigned int len;
    // prevrawlensize + lensize
    unsigned int headersize;
    // ZIP_STR_* 或者 ZIP_INT_*
    unsigned char encoding;
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;

pre_entry_length

記錄前一個節點的長度。通過這個值,可以進行指針計算,從而跳轉到上一個節點

area        |<---- previous entry --->|<--------------- current entry ---------------->|

size          5 bytes                   1 byte             ?          ?        ?
            +-------------------------+-----------------------------+--------+---------+
component   | ...                     | pre_entry_length | encoding | length | content |
            |                         |                  |          |        |         |
value       |                         | 0000 0101        |    ?     |   ?    |    ?    |
            +-------------------------+-----------------------------+--------+---------+
            ^                         ^
address     |                         |
            p = e - 5                 e

以上圖爲例,從當前節點的指針 e,減去 pre_entry_length 的值(0000 0101 的十進制值,5),就可以得到指向前一個節點的地址 p。

encoding 和 length

encodinglength 兩部分一起決定了 content 部分所保存的數據的類型(以及長度)。

其中, encoding 域的長度爲兩個 bit , 它的值可以是 00011011

  • 000110 表示 content 部分保存着字符數組。
  • 11 表示 content 部分保存着整數。

000110 開頭的字符數組的編碼方式如下:

表格中的下劃線 _ 表示留空,而變量 b 、 x 等則代表實際的二進制數據。爲了方便閱讀,多個字節之間用空格隔開。

11 開頭的整數編碼如下:

content

content 部分保存着節點的內容,類型和長度由 encodinglength 決定。

以下是一個保存着字符數組 hello world 的節點的例子:

area      |<---------------------- entry ----------------------->|

size        ?                  2 bit      6 bit    11 byte
          +------------------+----------+--------+---------------+
component | pre_entry_length | encoding | length | content       |
          |                  |          |        |               |
value     | ?                |    00    | 001011 | hello world   |
          +------------------+----------+--------+---------------+

encoding 域的值 00 表示節點保存着一個長度小於等於 63 字節的字符數組, length 域給出了這個字符數組的準確長度 —— 11 字節(的二進制 001011), content 則保存着字符數組值 hello world 本身(爲了方便表示, content 部分使用字符而不是二進制表示)。

以下是另一個節點,它保存着整數 10086

area      |<---------------------- entry ----------------------->|

size        ?                  2 bit      6 bit    2 bytes
          +------------------+----------+--------+---------------+
component | pre_entry_length | encoding | length | content       |
          |                  |          |        |               |
value     | ?                |    11    | 000000 | 10086         |
          +------------------+----------+--------+---------------+

encoding 域的值 11 表示節點保存的是一個整數; 而 length 域的值 000000 表示這個節點的值的類型爲 int16_t ; 最後, content 保存着整數值 10086 本身(爲了方便表示, content 部分用十進制而不是二進制表示)。

ziplist 基本操作

創建新 ziplist

創建一個新的 ziplist,時間複雜度是 O(1)。

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
    // header 和 end 需要的字節數
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    // 分配 bytes 長度的內存
    unsigned char *zl = zmalloc(bytes);
    // 賦值 zlbytes
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    // 賦值 zltail
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    // 賦值 zllen
    ZIPLIST_LENGTH(zl) = 0;
    // 賦值 zlend
    zl[bytes-1] = ZIP_END;
    return zl;
}
area        |<---- ziplist header ---->|<-- end -->|

size          4 bytes   4 bytes 2 bytes  1 byte
            +---------+--------+-------+-----------+
component   | zlbytes | zltail | zllen | zlend     |
            |         |        |       |           |
value       |  1011   |  1010  |   0   | 1111 1111 |
            +---------+--------+-------+-----------+
                                       ^
                                       |
                               ZIPLIST_ENTRY_HEAD
                                       &
address                        ZIPLIST_ENTRY_TAIL
                                       &
                               ZIPLIST_ENTRY_END

空白 ziplist 的表頭、表尾和末端處於同一地址。

創建了 ziplist 之後, 就可以往裏面添加新節點了, 根據新節點添加位置的不同, 這個工作可以分爲兩類來進行:

  1. 將節點添加到 ziplist 末端:在這種情況下,新節點的後面沒有任何節點。
  2. 將節點添加到某個/某些節點的前面:在這種情況下,新節點的後面有至少一個節點。

以下兩個小節分別討論這兩種情況。

將節點添加到末端

將新節點添加到 ziplist 的末端需要執行以下三個步驟:

  1. 記錄到達 ziplist 末端所需的偏移量(因爲之後的內存重分配可能會改變 ziplist 的地址,因此記錄偏移量而不是保存指針)
  2. 根據新節點要保存的值,計算出編碼這個值所需的空間大小,以及編碼它前一個節點的長度所需的空間大小,然後對 ziplist 進行內存重分配。
  3. 設置新節點的各項屬性: pre_entry_lengthencodinglengthcontent
  4. 更新 ziplist 的各項屬性,比如記錄空間佔用的 zlbytes ,到達表尾節點的偏移量 zltail ,以及記錄節點數量的 zllen

舉個例子,假設現在要將一個新節點添加到只含有一個節點的 ziplist 上,程序首先要執行步驟 1 ,定位 ziplist 的末端:

area        |<---- ziplist header ---->|<--- entries -->|<-- end -->|

size          4 bytes  4 bytes  2 bytes  5 bytes          1 bytes
            +---------+--------+-------+----------------+-----------+
component   | zlbytes | zltail | zllen | entry 1        | zlend     |
            |         |        |       |                |           |
value       |  10000  |  1010  |   1   | ?              | 1111 1111 |
            +---------+--------+-------+----------------+-----------+
                                       ^                ^
                                       |                |
address                         ZIPLIST_ENTRY_HEAD   ZIPLIST_ENTRY_END
                                       &
                                ZIPLIST_ENTRY_TAIL

然後執行步驟 2 ,程序需要計算新節點所需的空間:

假設我們要添加到節點裏的值爲字符數組 hello world , 那麼保存這個值共需要 12 字節的空間:

  • 11 字節用於保存字符數組本身;
  • 另外 1 字節中的 2 bit 用於保存類型編碼 00 , 而其餘 6 bit 則保存字符數組長度 11 的二進制 001011

另外,節點還需要 1 字節, 用於保存前一個節點的長度 5 (二進制 101 )。

合算起來,爲了添加新節點, ziplist 總共需要多分配 13 字節空間。 以下是分配完成之後, ziplist 的樣子:

area        |<---- ziplist header ---->|<------------ entries ------------>|<-- end -->|

size          4 bytes  4 bytes  2 bytes  5 bytes          13 bytes           1 bytes
            +---------+--------+-------+----------------+------------------+-----------+
component   | zlbytes | zltail | zllen | entry 1        | entry 2          | zlend     |
            |         |        |       |                |                  |           |
value       |  10000  |  1010  |   1   | ?              | pre_entry_length | 1111 1111 |
            |         |        |       |                | ?                |           |
            |         |        |       |                |                  |           |
            |         |        |       |                | encoding         |           |
            |         |        |       |                | ?                |           |
            |         |        |       |                |                  |           |
            |         |        |       |                | length           |           |
            |         |        |       |                | ?                |           |
            |         |        |       |                |                  |           |
            |         |        |       |                | content          |           |
            |         |        |       |                | ?                |           |
            |         |        |       |                |                  |           |
            +---------+--------+-------+----------------+------------------+-----------+
                                       ^                ^
                                       |                |
address                       ZIPLIST_ENTRY_HEAD   ZIPLIST_ENTRY_END
                                       &
                              ZIPLIST_ENTRY_TAIL

步驟三,更新新節點的各項屬性(爲了方便表示, content 的內容使用字符而不是二進制來表示):

area        |<---- ziplist header ---->|<------------ entries ------------>|<-- end -->|

size          4 bytes  4 bytes  2 bytes  5 bytes          13 bytes           1 bytes
            +---------+--------+-------+----------------+------------------+-----------+
component   | zlbytes | zltail | zllen | entry 1        | entry 2          | zlend     |
            |         |        |       |                |                  |           |
value       |  10000  |  1010  |   1   | ?              | pre_entry_length | 1111 1111 |
            |         |        |       |                | 101              |           |
            |         |        |       |                |                  |           |
            |         |        |       |                | encoding         |           |
            |         |        |       |                | 00               |           |
            |         |        |       |                |                  |           |
            |         |        |       |                | length           |           |
            |         |        |       |                | 001011           |           |
            |         |        |       |                |                  |           |
            |         |        |       |                | content          |           |
            |         |        |       |                | hello world      |           |
            |         |        |       |                |                  |           |
            +---------+--------+-------+----------------+------------------+-----------+
                                       ^                ^
                                       |                |
address                       ZIPLIST_ENTRY_HEAD   ZIPLIST_ENTRY_END
                                       &
                              ZIPLIST_ENTRY_TAIL

最後一步,更新 ziplist 的 zlbytes 、 zltail 和 zllen 屬性:

area        |<---- ziplist header ---->|<------------ entries ------------>|<-- end -->|

size          4 bytes  4 bytes  2 bytes  5 bytes          13 bytes           1 bytes
            +---------+--------+-------+----------------+------------------+-----------+
component   | zlbytes | zltail | zllen | entry 1        | entry 2          | zlend     |
            |         |        |       |                |                  |           |
value       |  11101  |  1111  |  10   | ?              | pre_entry_length | 1111 1111 |
            |         |        |       |                | 101              |           |
            |         |        |       |                |                  |           |
            |         |        |       |                | encoding         |           |
            |         |        |       |                | 00               |           |
            |         |        |       |                |                  |           |
            |         |        |       |                | length           |           |
            |         |        |       |                | 001011           |           |
            |         |        |       |                |                  |           |
            |         |        |       |                | content          |           |
            |         |        |       |                | hello world      |           |
            |         |        |       |                |                  |           |
            +---------+--------+-------+----------------+------------------+-----------+
                                       ^                ^                  ^
                                       |                |                  |
address                                |          ZIPLIST_ENTRY_TAIL   ZIPLIST_ENTRY_END
                                       |
                               ZIPLIST_ENTRY_HEAD

將節點添加到某個/某些節點的前面

比起將新節點添加到 ziplist 的末端, 將一個新節點添加到某個/某些節點的前面要複雜得多, 因爲這種操作除了將新節點添加到 ziplist 以外, 還可能引起後續一系列節點的改變。

舉個例子,假設我們要將一個新節點 new 添加到節點 prevnext 之間:

   add new entry here
           |
           V
+----------+----------+----------+----------+----------+
|          |          |          |          |          |
|   prev   |   next   | next + 1 | next + 2 |   ...    |
|          |          |          |          |          |
+----------+----------+----------+----------+----------+

程序首先爲新節點擴大 ziplist 的空間:

+----------+----------+----------+----------+----------+----------+
|          |          |          |          |          |          |
|   prev   |   ???    |   next   | next + 1 | next + 2 |   ...    |
|          |          |          |          |          |          |
+----------+----------+----------+----------+----------+----------+

           |<-------->|
              expand
              space

然後設置 new 節點的各項值 —— 到目前爲止,一切都和前面介紹的添加操作一樣:

             set value,
             property,
             length,
             etc.
                |
                v
+----------+----------+----------+----------+----------+----------+
|          |          |          |          |          |          |
|   prev   |   new    |   next   | next + 1 | next + 2 |   ...    |
|          |          |          |          |          |          |
+----------+----------+----------+----------+----------+----------+

現在,新的 new 節點取代原來的 prev 節點, 成爲了 next 節點的新前驅節點, 不過, 因爲這時 next 節點的 pre_entry_length 域編碼的仍然是 prev 節點的長度, 所以程序需要將 new 節點的長度編碼進 next 節點的 pre_entry_length 域裏, 這裏會出現三種可能:

  1. nextpre_entry_length 域的長度正好能夠編碼 new 的長度(都是 1 字節或者都是 5 字節)
  2. nextpre_entry_length 只有 1 字節長,但編碼 new 的長度需要 5 字節
  3. nextpre_entry_length 有 5 字節長,但編碼 new 的長度只需要 1 字節

對於情況 1 和 3 , 程序直接更新 nextpre_entry_length 域。

如果是第二種情況, 那麼程序必須對 ziplist 進行內存重分配, 從而擴展 next 的空間。 然而,因爲 next 的空間長度改變了, 所以程序又必須檢查 next 的後繼節點 —— next+1 , 看它的 pre_entry_length 能否編碼 next 的新長度, 如果不能的話,程序又需要繼續對 next+1 進行擴容。

這就是說, 在某個/某些節點的前面添加新節點之後, 程序必須沿着路徑挨個檢查後續的節點,是否滿足新長度的編碼要求, 直到遇到一個能滿足要求的節點(如果有一個能滿足,則這個節點之後的其他節點也滿足), 或者到達 ziplist 的末端 zlend 爲止。

刪除節點

刪除節點和添加操作的步驟類似。

1) 定位目標節點,並計算節點的空間長度 target-size

   target start here
           |
           V
+----------+----------+----------+----------+----------+----------+
|          |          |          |          |          |          |
|   prev   |  target  |   next   | next + 1 | next + 2 |   ...    |
|          |          |          |          |          |          |
+----------+----------+----------+----------+----------+----------+

           |<-------->|
            target-size

2) 進行內存移位,覆蓋 target 原本的數據,然後通過內存重分配,收縮多餘空間:

   target start here
           |
           V
+----------+----------+----------+----------+----------+
|          |          |          |          |          |
|   prev   |   next   | next + 1 | next + 2 |   ...    |
|          |          |          |          |          |
+----------+----------+----------+----------+----------+

           | <------------------------------------------ memmove

3) 檢查 nextnext+1 等後續節點能否滿足新前驅節點的編碼。和添加操作一樣,刪除操作也可能會引起連鎖更新。

參考鏈接

Redis 源碼簡潔剖析系列

最簡潔的 Redis 源碼剖析系列文章

Java 編程思想-最全思維導圖-GitHub 下載鏈接,需要的小夥伴可以自取~

原創不易,希望大家轉載時請先聯繫我,並標註原文鏈接。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章