走馬觀花說說Redis的ziplist的奧祕

本篇博客參考:html

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

Redis內部數據結構詳解(4)——ziplistnode

Redis的壓縮列表ZipListpython

上篇博客中,我給你們走馬觀花般的介紹了Redis中SDS的奧祕,說明Redis之因此那麼快,還有一個很重要、可是常常被你們忽視的一點,那就是Redis精心設計的數據結構。本篇博客,仍是繼續這個話題,給你們介紹下Redis另一種底層數據結構:ziplist。git

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

ziplist是什麼意思

我剛開始看ziplist的時候,總以爲zip這個單詞甚是熟悉,好像在平常使用電腦的時候常常看到,因而我百度了下:
image.png
哦哦,怪不得那麼熟悉,原來就是「壓縮」的意思,那ziplist就能夠翻譯成「壓縮列表」了。web

爲何要有ziplist

有兩點緣由:redis

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

來看看ziplist的存在

zadd programmings 1.0 go 2.0 python 3.0 java

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

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 就必須用標準結構存儲

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

ziplist元素太多,怎麼辦

在介紹ziplist佈局的時候,說到ziplist用兩個位來記錄ziplist的元素個數,若是元素個數實在太多,兩個位不夠怎麼辦呢?這種狀況下,求ziplist元素的個數只能遍歷了。

看到了吧,Redis真不是想象中的那麼簡單,須要研究的東西仍是挺多,也挺複雜的,若是咱們不去學習,可能以爲本身徹底掌握了Redis,可是一旦開始學習了,才發現咱們先前掌握的只是皮毛。驗證了一句話,知道的越多,不知道的越多。

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

相關文章
相關標籤/搜索