蜻蜓點水說說Redis的ziplist的奧祕

上篇博客中,我給大家蜻蜓點水般的介紹了Redis中SDS的奧祕,說明Redis之所以那麼快,還有一個很重要、但是經常被大家忽視的一點,那就是Redis精心設計的數據結構。本篇博客,還是繼續這個話題,給大家介紹下Redis另外一種底層數據結構:ziplist。

在Redis中,有五種基本數據類型,除了上篇博客提到的String,還有list,hash,zset,set,其中list,hash,zset都間接或者直接使用了ziplist,所以說理解ziplist也是相當重要的。

ziplist是什麼意思

我剛開始看ziplist的時候,總覺得zip這個單詞甚是熟悉,好像在日常使用電腦的時候經常看到,於是我百度了下:



哦哦,怪不得那麼熟悉,原來就是“壓縮”的意思,那ziplist就可以翻譯成“壓縮列表”了。

爲什麼要有ziplist

有兩點原因:

  • 普通的雙向鏈表,會有兩個指針,在存儲數據很小的情況下,我們存儲的實際數據的大小可能還沒有指針佔用的內存大,是不是有點得不償失?而且Redis是基於內存的,而且是常駐內存的,內存是彌足珍貴的,所以Redis的開發者們肯定要使出渾身解數優化佔用內存,於是,ziplist出現了。
  • 鏈表在內存中,一般是不連續的,遍歷相對比較慢,而ziplist很好的解決的這個問題。

來看看ziplist的存在

zadd programmings 1.0 go 2.0 python 3.0 java

創建了一個zset,裏面有三個元素,然後看下它採用的數據結構:

debug object  programmings
"Value at:0x7f404ac30c60 refcount:1 encoding:ziplist serializedlength:36 lru:2689815 lru_seconds_idle:9"
HSET website google "www.g.cn

創建了一個hash,只有一個元素,看下它採用的數據結構:

debug object website
"Value at:0x7f404ac30ac0 refcount:1 encoding:ziplist serializedlength:30 lru:2690274 lru_seconds_idle:14"

可以很清楚的看到,zset和hash都採用了ziplist數據結構。

當滿足一定的條件,zset和hash就不再使用ziplist數據結構了:


debug object website
"Value at:0x7f404ac30ac0 refcount:1 encoding:hashtable serializedlength:180 lru:2690810 lru_seconds_idle:2"

可以看到,hash的底層數據結構變成了hashtable。

szet就不做實驗了,感興趣的小夥伴們可以自己實驗下。

至於這個轉換條件是什麼,放到後面再說。

好奇的你們,肯定會嘗試看下list的底層數據結構是什麼,發現並不是ziplist:

LPUSH languages python
debug object languages
"Value at:0x7f404c4763d0 refcount:1 encoding:quicklist serializedlength:21 lru:2691722 lru_seconds_idle:22 ql_nodes:1 ql_avg_node:1.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:19"

可以看到,list採用的底層數據結構是quicklist,並不是ziplist。

在低版本的Redis中,list採用的底層數據結構是ziplist+linkedList,高版本的Redis中,quicklist替換了ziplist+linkedList,而quicklist也用到了ziplist,所以可以說list間接使用了ziplist數據結構。這個quicklist是什麼,不是本篇博客的內容,暫且不表。

探究ziplist

ziplist源碼:ziplist源碼
ziplist源碼的註釋寫的非常清楚,如果英語比較好,可以直接看上面的註釋,如果你英語不是太好,或者沒有一定的鑽研精神,還是看看我寫的博客吧。

ziplist佈局

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>

這是在註釋中說明的ziplist佈局,我們一個個來看,這些字段是什麼:

  • zlbytes:32bit無符號整數,表示ziplist佔用的字節總數(包括<zlbytes>本身佔用的4個字節);
  • zltail:32bit無符號整數,記錄最後一個entry的偏移量,方便快速定位到最後一個entry;
  • zllen:16bit無符號整數,記錄entry的個數;
  • entry:存儲的若干個元素,可以爲字節數組或者整數;
  • zlend:ziplist最後一個字節,是一個結束的標記位,值固定爲255。

Redis通過以下宏定義實現了對ziplist各個字段的存取:

// 假設char *zl 指向ziplist首地址
// 指向zlbytes字段
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))

// 指向zltail字段(zl+4)
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

// 指向zllen字段(zl+(4*2))
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

// 指向ziplist中尾元素的首地址
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

// 指向zlend字段,指恆爲255(0xFF)
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

entry的構成

從ziplist佈局中,我們可以很清楚的知道,我們的數據被保存在ziplist中的一個個entry中,我們下面來看看entry的構成。

<prevlen> <encoding> <entry-data>

我們再來看看這三個字段是什麼:

  • prevlen:前一個元素的字節長度,便於快速找到前一個元素的首地址,假如當前元素的首地址是x,那麼(x-prevlen)就是前一個元素的首地址。
  • encoding:當前元素的編碼,這個字段實在是太複雜了,我們放到後面再說;
  • entry-data:實際存儲的數據。
prevlen

prevlen字段是變長的:

  • 前一個元素的長度小於254字節時,prevlen用1個字節表示;
  • 前一個元素的長度大於等於254字節時,prevlen用5個字節進行表示,此時,prevlen的第一個字節是固定的254(0xFE)(作爲這種情況的一個標誌),後面4個字節才表示前一個元素的長度。
encoding

下面就要介紹下encoding這個字段了,在此之前,大家可以到陽臺吹吹風,喝口熱水,再做個深呼吸,最後再做一個心理準備,因爲這個字段實在是太複雜了,搞不好,看的時候,一下子吐了。。。如果實在無法理解,直接略過這一段吧。

Redis爲了節約空間,對encoding字段進行了相當複雜的設計,Redis通過encoding來判斷存儲數據的類型,下面我們就來看看Redis是如何根據encoding來判斷存儲數據的類型的:

  1. 00xxxxxx 最大長度位 63 的短字符串,後面的6個位存儲字符串的位數;
  2. 01xxxxxx xxxxxxxx 中等長度的字符串,後面14個位來表示字符串的長度;
  3. 10000000 aaaaaaaa bbbbbbbb cccccccc dddddddd 特大字符串,需要使用額外 4 個字節來表示長度。第一個字節前綴是10,剩餘 6 位沒有使用,統一置爲零;
  4. 11000000 表示 int16;
  5. 11010000 表示 int32;
  6. 11100000 表示 int64;
  7. 11110000 表示 int24;
  8. 11111110 表示 int8;
  9. 11111111 表示 ziplist 的結束,也就是 zlend 的值 0xFF;
  10. 1111xxxx 表示極小整數,xxxx 的範圍只能是 (0001~1101), 也就是1~13

如果是第10種情況,那麼entry的構成就發生變化了:

<prevlen> <encoding> 

因爲數據已經存儲在encoding字段中了。

可以看出Redis根據encoding字段的前兩位來判斷存儲的數據是字符串(字節數組)還是整型,如果是字符串,還可以通過encoding字段的前兩位來判斷字符串的長度;如果是整形,則要通過後面的位來判斷具體長度。

entry的結構體

我們上面說了那麼多關於entry的點點滴滴,下面將要說的內容可能會顛覆你三觀,雖然我們在源碼中可以看到entry的結構體,但是上面有一個註釋非常重要:

/* We use this function to receive information about a ziplist entry.
 * Note that this is not how the data is actually encoded, is just what we
 * get filled by a function in order to operate more easily. */
typedef struct zlentry {
    unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
    unsigned int prevrawlen;     /* Previous entry len. */
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    unsigned int headersize;     /* prevrawlensize + lensize. */
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;

重點看上面的註釋。一句話解釋:這個結構體雖然定義出來了,但是沒有被使用。

ziplist的存儲形式

Redis並沒有像上篇博客介紹的SDS一樣,封裝一個結構體來保存ziplist,而是通過定義一系列宏來對數據進行操作,也就是說ziplist是一堆字節數據,上面所說的ziplist的佈局和ziplist中的entry的佈局只是抽象出來的概念。

爲什麼不能一直是ziplist

在文章比較前面的部分,我們做了實驗來證明,滿足一定的條件後,zset、hash的底層存儲結構不再是ziplist,既然ziplist那麼牛逼,Redis的開發者也花了那麼多精力在ziplist的設計上面,爲什麼zset、hash的底層存儲結構不能一直是ziplist呢?
因爲ziplist是緊湊存儲,沒有冗餘空間,意味着新插入元素,就需要擴展內存,這就分爲兩種情況,一種是分配新的內存,將原數據拷貝到新內存;一種是擴展原有內存,所以ziplist 不適合存儲大型字符串,存儲的元素也不宜過多。

ziplist存儲界限

那麼滿足什麼條件後,zset、hash的底層存儲結構不再是ziplist呢?在配置文件中可以進行設置:

hash-max-ziplist-entries 512  # hash 的元素個數超過 512 就必須用標準結構存儲
hash-max-ziplist-value 64  # hash 的任意元素的 key/value 的長度超過 64 就必須用標準結構存儲
zset-max-ziplist-entries 128  # zset 的元素個數超過 128 就必須用標準結構存儲
zset-max-ziplist-value 64  # zset 的任意元素的長度超過 64 就必須用標準結構存儲

對於這個配置,我只是一個搬運工,並沒有去實驗,畢竟沒有人會去修改這個吧,感興趣的小夥伴可以試驗下。

看到了吧,Redis真不是想象中的那麼簡單,需要研究的東西還是挺多,也挺複雜的,如果我們不去學習,可能覺得自己完全掌握了Redis,但是一旦開始學習了,才發現我們先前掌握的只是皮毛。驗證了一句話,知道的越多,不知道的越多。

本篇博客到這裏就結束了、

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