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

CodeBear發表於2020-08-06

本篇部落格參考:

Redis 深度歷險:核心原理與應用實踐

Redis內部資料結構詳解(4)——ziplist

Redis的壓縮列表ZipList

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

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

ziplist是什麼意思

我剛開始看ziplist的時候,總覺得zip這個單詞甚是熟悉,好像在日常使用電腦的時候經常看到,於是我百度了下:
image.png
哦哦,怪不得那麼熟悉,原來就是“壓縮”的意思,那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資料結構了:
image.png

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佔用的位元組總數(包括本身佔用的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;

重點看上面的註釋。一句話解釋:這個結構體雖然定義出來了,但是沒有被使用,因為如果真的這麼使用的話,那麼entry佔用的記憶體就太大了。

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,但是一旦開始學習了,才發現我們先前掌握的只是皮毛。驗證了一句話,知道的越多,不知道的越多。

本篇部落格到這裡就結束了。

相關文章