redis 基礎數據結構 之壓縮列表

給新觀衆老爺的開場

大家好,我是弟弟!
最近讀了一遍 黃健宏大佬的 <<Redis 設計與實現>>,對Redis 3.0版本有了一些認識,該書作者有一版添加了註釋的 redis 3.0源碼
👉官方redis的github傳送門
👉黃健宏大佬添加了註釋的 redis 3.0源碼傳送門

網上說Redis代碼寫得很好,爲了加深印象和學習redis大佬的代碼寫作藝術,瞭解工作中使用的redis 命令背後的源碼邏輯,便有了從redis命令角度學習redis源碼的想法。
(全文提到的redis服務器,都指在 mac os 上啓動的一個默認配置的單機redis服務器)

ziplist是什麼?

redis的鏈表數據類型,在底層有兩種數據結構實現.
一種是上一篇講到list數據結構,
另外一種就是 壓縮鏈表 ziplist
ziplist的結構較爲複雜,產生了一系列相關的稍顯複雜的代碼。
但從功能層面看 ziplist,list 都能實現鏈表的功能。

那讓我們來看看ziplist具體是什麼

弟弟:“從名字上看,壓縮列表,就是一個被壓縮了的列表”
觀衆老爺: “…,要你有何用,你不說我們也能看出來。😡”
弟弟:"…不好意思,打擾了🙃️"
弟弟: “redis的壓縮列表,存儲在連續的內存空間中,沒有代碼層面的數據結構定義,依賴設計好的二進制格式來進行操作。
觀衆老爺:“你這講得不夠專業啊,也不形象。”
弟弟:“😭,好的瞭解。
那讓我們來 結合 redis裏的英文註釋 和 黃健宏大佬的中文註釋 來看看是怎麼講的”
(ps: 該註釋出現在 ziplist.c 文件中)

ziplist的設計目的

👇

 * The ziplist is a specially encoded dually linked list that is designed
 * to be very memory efficient. 
 * Ziplist 是爲了儘可能地節約內存而設計的特殊編碼雙端鏈表。

ziplist的作用

👇

 * It stores both strings and integer values,
 * where integers are encoded as actual integers instead of a series of
 * characters. 
 * Ziplist 可以儲存字符串值和整數值,
 * 其中,整數值被保存爲實際的整數,而不是字符數組。

ziplist的小瑕疵

👇

 * 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 佔用的內存大小有關。

ziplist 的空間佈局

ziplist的空間佈局,按低地址向高地址的方向 如下

  1. 開頭用 四個字節來存放ziplist的字節數大小
  2. 接着用 四個字節來存放ziplist尾節點的偏移字節數
  3. 然後是 2個字節表示該ziplist裏放了多少個節點
  4. 接着就是 <節點>,<節點>,<節點>... 0到n個
  5. 最後是 一個字節的ziplist結束標誌 255

👇看一遍註釋

 * ZIPLIST OVERALL LAYOUT:
 * Ziplist 的整體佈局:
 * The general layout of the ziplist is as follows:
 * 以下是 ziplist 的一般佈局:
 * <zlbytes><zltail><zllen><entry><entry><zlend>

zlbytes的含義

 * <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 使用的內存數量。
 * 通過這個值,程序可以直接對 ziplist 的內存大小進行調整,
 * 而無須爲了計算 ziplist 的內存大小而遍歷整個列表。

zltail的含義

 * <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的含義

 * <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> 保存着列表中的節點數量。
 * 當 zllen 保存的值大於 2**16-2 時,(大於等於 65535 時)
 * 程序需要遍歷整個列表才能知道列表實際包含了多少個節點。

zlend的含義

* <zlend> is a single byte special value, equal to 255, which indicates the
* end of the list.
* <zlend> 的長度爲 1 字節,值爲 255 ,標識列表的末尾。

觀衆老爺:“wo*, 這個ziplist的格式有點複雜啊”
弟弟:“是啊,有點複雜,還有ziplist的節點格式沒說呢 ,
ziplist的節點格式 及 相關代碼 是造成ziplist複雜的真正元兇🙃️”

ziplist的節點格式

ziplist節點 的空間佈局,分爲兩部分部分。

  1. header
    1.1 描述前一個節點佔用的空間大小
    1.2 描述本節點的值的類型,以及值的長度
  2. 值本身

詳細結構,按低地址向高地址的方向 如下

  1. 1or5個字節用於描述前面一個節點的 字節數大小
    若第1個字節 小於 #define ZIP_BIGLEN 254,則該字節直接表示前一個節點的字節數大小
    否則爲5個字節,第2,3,4,5字節一起構成一個 32爲整數表示前一節點的大小

  2. 緊接着的 1or2or5 字節一共表示了三類信息
    1- 當前節點值類型
    2- 描述當前節點值的長度所需要的空間大小
    3- 當前節點值的長度
    這也太能省了,通過提高結構的複雜程度弄暈計算機來節省存儲空間 🙃️

    2.1 第一個字節叫 encoding
    如果 encoding 小於 #define ZIP_STR_MASK 0xc0(二進制 11000000),
    那該節點存的是字符串類型。
    那麼 描述該字符串長度所需要的存儲空間大小的類型 就是 encoding & ZIP_STR_MASK
    取值分別以下三種,

    `/** 字符串編碼類型
    #define ZIP_STR_MASK 0xc0                //0xc0  11000000
    #define ZIP_STR_06B (0 << 6)             //      00000000
    #define ZIP_STR_14B (1 << 6)             //      01000000
    #define ZIP_STR_32B (2 << 6)             //      10000000
    `
    

    6B,14B,32B的意思是,描述字符串長度所需要的空間分別是6位/14位/32位,
    且描述字符串的長度,是按大端序存放的,且前6位存放在了encoding裏,額外的空間緊接着encoding
    所以描述這三類信息, 字符串類型、存儲字符串長度的空間大小,以及字符串的長度 一共需要 1/2/5個字節

    如果一下就看懂了,那說明你真是個聰明的觀衆老爺呢😊
    🙃️如果看暈了,沒關係,我們對着整數類型再來一遍😊

    如果 encoding 大於等於 #define ZIP_STR_MASK 0xc0(二進制 11000000)
    那該節點的值是一個整數
    那麼 描述該字符串長度所需要的存儲空間大小 就是 encoding本身 👇

    /* 
    * 整數編碼類型
    *  */
    * #define ZIP_INT_16B (0xc0 | 0 << 4)     //      11000000
    * #define ZIP_INT_32B (0xc0 | 1 << 4)     //      11010000
    * #define ZIP_INT_64B (0xc0 | 2 << 4)     //      11100000
    * #define ZIP_INT_24B (0xc0 | 3 << 4)     //      11110000
    * #define ZIP_INT_8B 0xfe                 // 0xFE 11111110
    

    8B/16B/24B/32B/64B 對應的 值的長度 分別是 (8位/16位/24位/32位/64位)
    所以整數類型與字符串類型不同的地方在於
    ziplist整數類型節點,僅需要一個字節,就可以描述存儲的值的是整數類型,和值佔用的空間字節數

    整數還有一個隱藏類型,如果是整數[0,12], 則將值直接保存到了encoding中😏
    encoding取值在下面兩個值之間(包含),那麼encoding - ZIP_INT_IMM_MIN - 1 的結果就直接是整數值,自然地該節點不需要再存儲一份值。
    #define ZIP_INT_IMM_MIN 0xf1 /* 11110001 */
    #define ZIP_INT_IMM_MAX 0xfd /* 11111101 */

  3. 第三部分就是節點的值本身啦,在3.0版本中值被本身是沒有經過壓縮處理的。

瞭解了ziplist節點的結構,可以知道,ziplist是可以通過頭尾指針雙向遍歷的

下面是 ziplist節點格式的 官方描述👇

* ZIPLIST ENTRIES:
 * ZIPLIST 節點:
 * 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 節點的前面都帶有一個 header ,這個 header 包含兩部分信息:
 * 1)前置節點的長度,在程序從後向前遍歷時使用。
 * 2)當前節點所保存的值的類型和長度。
 * 
 * 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.
 * 編碼前置節點的長度的方法如下:
 * 1) 如果前置節點的長度小於 254 字節,那麼程序將使用 1 個字節來保存這個長度值。
 * 2) 如果前置節點的長度大於等於 254 字節,那麼程序將使用 5 個字節來保存這個長度值:
 *    a)1 個字節的值將被設爲 254 ,用於標識這是一個 5 字節長的長度值。
 *    b) 之後的 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:
 * header 另一部分的內容和節點所保存的值有關。
 * 1) 如果節點保存的是字符串值,
 *    那麼這部分 header 的頭 2 個位將保存編碼字符串長度所使用的類型,
 *    而之後跟着的內容則是字符串的實際長度。
 * |00pppppp| - 1 byte
 *      String value with length less than or equal to 63 bytes (6 bits).
 *      字符串的長度小於或等於 63 字節。
 * 
 * |01pppppp|qqqqqqqq| - 2 bytes
 *      String value with length less than or equal to 16383 bytes (14 bits).
 *      字符串的長度小於或等於 16383 字節。
 * |10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
 *      String value with length greater than or equal to 16384 bytes.
 *      字符串的長度大於或等於 16384 字節。
 *
 * 2) 如果節點保存的是整數值,
 *    那麼這部分 header 的頭 2 位都將被設置爲 1*    而之後跟着的 2 位則用於標識節點所保存的整數的類型。
 *
 * |11000000| - 1 byte
 *      Integer encoded as int16_t (2 bytes).
 *      節點的值爲 int16_t 類型的整數,長度爲 2 字節。
 * |11010000| - 1 byte
 *      Integer encoded as int32_t (4 bytes).
 *      節點的值爲 int32_t 類型的整數,長度爲 4 字節。
 * |11100000| - 1 byte
 *      Integer encoded as int64_t (8 bytes).
 *      節點的值爲 int64_t 類型的整數,長度爲 8 字節。
 * |11110000| - 1 byte
 *      Integer encoded as 24 bit signed (3 bytes).
 *      節點的值爲 24 位(3 字節)長的整數。
 * |11111110| - 1 byte
 *      Integer encoded as 8 bit signed (1 byte).
 *      節點的值爲 8 位(1 字節)長的整數。
 * |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.
 *      節點的值爲介於 012 之間的無符號整數。
 *      因爲 00001111 都不能使用,所以位的實際值將是 113*      程序在取得這 4 個位的值之後,還需要減去 1 ,才能計算出正確的值。
 *      比如說,如果位的值爲 0001 = 1 ,那麼程序返回的值將是 1 - 1 = 0* |11111111| - End of ziplist.
 *      ziplist 的結尾標識
 *
 * All the integers are represented in little endian byte order.
 *
 * 所有整數都表示爲小端字節序。

ziplist insert!

redis是有 key/value 數據庫的說法的。
既然ziplist這麼複雜,那我們就簡單一點,站在crud的角度擼它一下。
在創建ziplist 並 插入一個節點之前。
我們來看一些ziplist相關的源碼,回憶一下ziplist的結構

ziplist上的基本操作

  1. 查看一個ziplist的大小 (佔用的字節數)
// 定位到 ziplist 的 bytes 屬性,該屬性記錄了整個 ziplist 所佔用的內存字節數
// 用於取出 bytes 屬性的現有值,或者爲 bytes 屬性賦予新值
#define ZIPLIST_BYTES(zl) (*((uint32_t *)(zl)))
  1. 查看ziplist的尾節點的偏移量
// 定位到 ziplist 的 offset 屬性,該屬性記錄了到達表尾節點的偏移量
// 用於取出 offset 屬性的現有值,或者爲 offset 屬性賦予新值
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t *)((zl) + sizeof(uint32_t))))
  1. 查看ziplist的節點個數
// 定位到 ziplist 的 length 屬性,該屬性記錄了 ziplist 包含的節點數量
// 用於取出 length 屬性的現有值,或者爲 length 屬性賦予新值
#define ZIPLIST_LENGTH(zl) (*((uint16_t *)((zl) + sizeof(uint32_t) * 2)))
  1. 查看 ziplist 表頭的大小
// 返回 ziplist 表頭的大小
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t) * 2 + sizeof(uint16_t))
  1. 查看 ziplist 的第一個節點
// 返回指向 ziplist 第一個節點(的起始位置)的指針
#define ZIPLIST_ENTRY_HEAD(zl) ((zl) + ZIPLIST_HEADER_SIZE)
  1. 查看 ziplist 最後一個節點
// 返回指向 ziplist 最後一個節點(的起始位置)的指針
#define ZIPLIST_ENTRY_TAIL(zl) ((zl) + intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
  1. 查看 ziplist 的最後一個字節(結束標誌)
// 返回指向 ziplist 末端 ZIP_END (的起始位置)的指針
#define ZIPLIST_ENTRY_END(zl) ((zl) + intrev32ifbe(ZIPLIST_BYTES(zl)) - 1)

8.再來看下 黃健宏大佬給的ziplist結構示意圖👇

`/* 
空白 ziplist 示例圖

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 示例圖

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

size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                        ZIPLIST_ENTRY_TAIL
*/`

ziplist 節點上的基本操作

再正式insert前,再來看幾個 ziplist節點的 操作

  1. header中的前置節點的基本操作
    1.1 已知ziplist節點 指針ptr,求描述前置節點長度所需要的空間大小
/* Decode the number of bytes required to store the length of the previous
* element, from the perspective of the entry pointed to by 'ptr'. 
* 解碼 ptr 指針,
* 取出編碼前置節點長度所需的字節數,並將它保存到 prevlensize 變量中。
*/
#define ZIP_BIGLEN 254
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) \
   do                                           \
   {                                            \
       if ((ptr)[0] < ZIP_BIGLEN)               \
       {                                        \
           (prevlensize) = 1;                   \
       }                                        \
       else                                     \
       {                                        \
           (prevlensize) = 5;                   \
       }                                        \
   } while (0);

1.2 已知ziplist節點的 指針ptr,以及描述前置節點長度所需要的空間大小, 求前置節點長度

/* Decode the length of the previous element, from the perspective of the entry
 * pointed to by 'ptr'. 
 * 解碼 ptr 指針,
 * 取出編碼前置節點長度所需的字節數,
 * 並將這個字節數保存到 prevlensize 中。
 * 然後根據 prevlensize ,從 ptr 中取出前置節點的長度值,
 * 並將這個長度值保存到 prevlen 變量中。
 * T = O(1)
 */
#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen)    \
    do                                                   \
    {                                                    \
        /* 先計算被編碼長度值的字節數 */    \
        ZIP_DECODE_PREVLENSIZE(ptr, prevlensize);        \
        /* 再根據編碼字節數來取出長度值 */ \
        if ((prevlensize) == 1)                          \
        {                                                \
            (prevlen) = (ptr)[0];                        \
        }                                                \
        else if ((prevlensize) == 5)                     \
        {                                                \
            assert(sizeof((prevlensize)) == 4);          \
            memcpy(&(prevlen), ((char *)(ptr)) + 1, 4);  \
            memrev32ifbe(&prevlen);//如果是大端序,則轉成小端序                      \
        }                                                \
    } while (0);
  1. header中本節點值的類型與長度的基本操作
    2.1 已知 ptr (ziplist節點指針 + 存放前置節點信息佔用的空間大小), 求節點值的編碼類型
/* Extract the encoding from the byte pointed by 'ptr' and set it into
 * 'encoding'. 
 * 從 ptr 中取出節點值的編碼類型,並將它保存到 encoding 變量中。
 */
#define ZIP_STR_MASK 0xc0               //0xc0  11000000 1個字節
#define ZIP_ENTRY_ENCODING(ptr, encoding) \
    do                                    \
    {                                     \
        (encoding) = (ptr[0]);            \
        if ((encoding) < ZIP_STR_MASK)    \
            (encoding) &= ZIP_STR_MASK;   \
    } while (0)

2.2 已知 ptr (ziplist節點指針 + 存放前置節點信息佔用的空間大小), 和節點值的編碼類型
求存放節點值長度的空間大小,以及節點值的長度

/* Decode the length encoded in 'ptr'. The 'encoding' variable will hold the
* entries encoding, the 'lensize' variable will hold the number of bytes
* required to encode the entries length, and the 'len' variable will hold the
* entries length. 
* 解碼 ptr 指針,取出列表節點的相關信息,並將它們保存在以下變量中:
* - encoding 保存節點值的編碼類型。
* - lensize 保存編碼節點長度所需的字節數。
* - len 保存節點的長度。
*/
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len)                                   \
   do                                                                                   \
   {                                                                                    \
       /* 取出值的編碼類型 */                                                   \
       ZIP_ENTRY_ENCODING((ptr), (encoding));                                           \
       /* 字符串編碼 */                                                            \
       if ((encoding) < ZIP_STR_MASK)                                                   \
       {                                                                                \
           if ((encoding) == ZIP_STR_06B)                                               \
           {                                                                            \
               (lensize) = 1;                                                           \
               (len) = (ptr)[0] & 0x3f; /* 編碼長度1字節長  00111111*/           \
           }                                                                            \
           else if ((encoding) == ZIP_STR_14B)                                          \
           {                                                                            \
               (lensize) = 2; /* 編碼長度2字節長 */                              \
               (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1];                             \
           }                                                                            \
           else if (encoding == ZIP_STR_32B)                                            \
           {                                                                            \
               (lensize) = 5; /* 編碼長度5字節長,實際長度是後4個字節 */ \
               (len) = ((ptr)[1] << 24) |                                               \
                       ((ptr)[2] << 16) |                                               \
                       ((ptr)[3] << 8) |                                                \
                       ((ptr)[4]);                                                      \
           }                                                                            \
           else                                                                         \
           {                                                                            \
               assert(NULL);                                                            \
           }                                                                            \
           /* 整數編碼 */                                                           \
       }                                                                                \
       else                                                                             \
       {                                                                                \
           (lensize) = 1;                                                               \
           (len) = zipIntSize(encoding);                                                \
       }                                                                                \
   } while (0);
   
static unsigned int zipIntSize(unsigned char encoding)
{
   switch (encoding)
   {
   case ZIP_INT_8B:
       return 1;
   case ZIP_INT_16B:
       return 2;
   case ZIP_INT_24B:
       return 3;
   case ZIP_INT_32B:
       return 4;
   case ZIP_INT_64B:
       return 8;
   default:
       return 0; /* 4 bit immediate */
   }
   assert(NULL);
   return 0;
}

2.3 已知p指向一個ziplist節點,求ziplist節點的大小就很簡單了👇

/* Return the total number of bytes used by the entry pointed to by 'p'. 
 * 返回指針 p 所指向的節點佔用的字節數總和。
 */
static unsigned int zipRawEntryLength(unsigned char *p)
{
    unsigned int prevlensize, encoding, lensize, len;
    // 取出編碼前置節點的長度所需的字節數
    ZIP_DECODE_PREVLENSIZE(p, prevlensize);
    // 取出當前節點值的編碼類型,編碼節點值長度所需的字節數,以及節點值的長度
    ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
    // 計算節點佔用的字節數總和
    return prevlensize + lensize + len;
}

好了,到這裏我們知道了 ziplist節點的結構、ziplist節點header的讀取方法。
我們可以接着看整個ziplist節點是怎麼讀取的了。
因爲ziplist結構是緊湊型的一整塊兒數據,所以爲了臨時讀取ziplist節點,弄了一個zlentry結構出來。

`/*
* 保存 ziplist 節點信息的結構
*/
typedef struct zlentry
{
   // prevrawlen :前置節點的長度
   // prevrawlensize :編碼 prevrawlen 所需的字節大小
   unsigned int prevrawlensize, prevrawlen;
   // len :當前節點值的長度
   // lensize :編碼 len 所需的字節大小
   unsigned int lensize, len;
   // 當前節點 header 的大小
   // 等於 prevrawlensize + lensize
   unsigned int headersize;
   // 當前節點值所使用的編碼類型
   unsigned char encoding;
   // 指向當前節點的指針
   unsigned char *p;
} zlentry;

已知p指向一個ziplist節點,讀取ziplist節點👇

/* Return a struct with all information about an entry. 
*
* 將 p 所指向的列表節點的信息全部保存到 zlentry 中,並返回該 zlentry 。
*
* T = O(1)
*/
static zlentry zipEntry(unsigned char *p)
{
   zlentry e;
   // e.prevrawlensize 保存着編碼前一個節點的長度所需的字節數
   // e.prevrawlen 保存着前一個節點的長度
   // T = O(1)
   ZIP_DECODE_PREVLEN(p, e.prevrawlensize, e.prevrawlen);
   // p + e.prevrawlensize 將指針移動到列表節點本身
   // e.encoding 保存着節點值的編碼類型
   // e.lensize 保存着編碼節點值長度所需的字節數
   // e.len 保存着節點值的長度
   // T = O(1)
   ZIP_DECODE_LENGTH(p + e.prevrawlensize, e.encoding, e.lensize, e.len);
   // 計算頭結點的字節數
   e.headersize = e.prevrawlensize + e.lensize;
   // 記錄指針
   e.p = p;
   return e;
}

ziplist節點插入流程

因爲ziplist是一個鏈表,給一個鏈表節點後插入一個新的鏈表節點,是一個比較好理解的事情。
但是ziplist是在連續空間存放的
插入ziplist節點,跟在數組中插入一個元素有點類似。
需要將ziplist後面部分空間整體向後移動,給新ziplist節點騰出空間。
但ziplist畢竟不是數組,它有一些特點…

在位置p插入一個新的ziplist節點的流程

  1. 計算新節點的大小 (header+值), 記爲 reqlen
  2. 計算後置節點header中(新節點後面的一個節點),存放新加入節點需要的額外空間 nextdiff
    nextdiff = 新值-舊值
  3. 對ziplist進行空間分配 在原大小的基礎上 加上 新節點大小 以及 nextdiff (0 or 4字節)
  4. 將p + nextdiff 到 原ziplist結束標記之間的值,整體移動到 從p+reqlen開始的空間
  5. 修正ziplist結構中,各個元素的值。比如 尾節點偏移量,元素個數等

因加入一個ziplist節點可能會影響到後置節點header中存放前置節點長度的空間大小和值
後置節點header中存放前置節點長度的空間可能變大(該空間只變大,不縮小)
將有可能對後續所有節點產生連鎖影響
於是, 一輪 對後續所有節點header中存放前置節點長度的空間與值 的修正將被觸發…

好吧,不得不說ziplist是有點小複雜。
因其結構稍顯複雜,代碼邏輯有點小繞,
本弟弟也是看了好久的ziplist節點的insert代碼才搞清楚是怎麼回事。

插入ziplist節點完整詳細的流程可以 看👉黃健宏大佬添加了註釋的 redis 3.0源碼 中的
ziplist.c/ziplistInsert 函數。 😂

ziplist搞這麼複雜,有用嗎?

既然redis作者說了,搞這個ziplist是爲了節省空間的。
ziplist、list 在3.0版本 對值都沒有進行壓縮。
所以節省的空間 是ziplist、list結構本身所佔用的空間

那麼我們就來看一下ziplist,list在相同個數以及相同大小的值的情況下,結構佔用的空間大小。

ziplist結構相比list結構的空間節省比例

從lpush命令對應的源碼可以知道
當創建一個新的鏈表時,默認創建一個ziplist。
當ziplist中的元素個數超過512個,或者元素值長度超過64字節時,(默認配置,可通過配置文件修改)
redis將ziplist轉換成list來存儲,
(因爲ziplist的結構特性,使得元素個數過大,或者元素值過大時,帶來額外的內存操作影響性能)

結論

  1. 512個元素 長度爲 1到63個字節 的情況下,節省空間比例高達 91.6%
    ziplist結構本身佔用1035字節空間,list結構本身佔用12336字節,

    ziplist的1035字節 = 4+4+2+2*512+1 (回憶一下ziplist的結構)
    list 的 12336字節= 48 + 24*512 (回憶一下list的結構)
    (ps: 64位系統上,任意一個指針變量佔用的空間是8個字節)

小結

  1. 鏈表的特性有一點像go語言裏的channel,一頭寫入,一頭讀取
  2. 可以當成隊列/棧用
  3. 做一些跟時間線相關的實時列表,
    比如最近10位閱讀本文的觀衆老爺,或者
    遊戲裏的最近充值VIP的100位RMB玩家。😂
  4. 可以做一些非實時的具有分頁需求的列表,
    比如直播平臺的人氣/打賞 TOP100. 小時榜,日榜,周榜。
  5. 消息隊列的話,還是用專業的好一點。🙃️
  6. 另外,似乎在3.2版本之後,
    在鏈表創建的時候,默認創建的是快速鏈表
    是的 ziplist跟list合體進化成了quicklist,似乎數據還能壓縮呢

往期博客回顧

  1. redis服務器的部分啓動過程
  2. GET命令背後的源碼邏輯
  3. redis的基礎數據結構之 sds
  4. redis的基礎數據結構之 list
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章