本篇博客參考:html
Redis的壓縮列表ZipListpython
上篇博客中,我給你們走馬觀花般的介紹了Redis中SDS的奧祕,說明Redis之因此那麼快,還有一個很重要、可是常常被你們忽視的一點,那就是Redis精心設計的數據結構。本篇博客,仍是繼續這個話題,給你們介紹下Redis另一種底層數據結構:ziplist。git
在Redis中,有五種基本數據類型,除了上篇博客提到的String,還有list,hash,zset,set,其中list,hash,zset都間接或者直接使用了ziplist,因此說理解ziplist也是至關重要的。github
我剛開始看ziplist的時候,總以爲zip這個單詞甚是熟悉,好像在平常使用電腦的時候常常看到,因而我百度了下:
哦哦,怪不得那麼熟悉,原來就是「壓縮」的意思,那ziplist就能夠翻譯成「壓縮列表」了。web
有兩點緣由:redis
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數據結構了:
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源碼的註釋寫的很是清楚,若是英語比較好,能夠直接看上面的註釋,若是你英語不是太好,或者沒有必定的鑽研精神,仍是看看我寫的博客吧。
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
這是在註釋中說明的ziplist佈局,咱們一個個來看,這些字段是什麼:
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)
從ziplist佈局中,咱們能夠很清楚的知道,咱們的數據被保存在ziplist中的一個個entry中,咱們下面來看看entry的構成。
<prevlen> <encoding> <entry-data>
咱們再來看看這三個字段是什麼:
prevlen字段是變長的:
下面就要介紹下encoding這個字段了,在此以前,你們能夠到陽臺吹吹風,喝口熱水,再作個深呼吸,最後再作一個心理準備,由於這個字段實在是太複雜了,搞很差,看的時候,一會兒吐了。。。若是實在沒法理解,直接略過這一段吧。
Redis爲了節約空間,對encoding字段進行了至關複雜的設計,Redis經過encoding來判斷存儲數據的類型,下面咱們就來看看Redis是如何根據encoding來判斷存儲數據的類型的:
00xxxxxx
最大長度位 63 的短字符串,後面的6個位存儲字符串的位數;01xxxxxx xxxxxxxx
中等長度的字符串,後面14個位來表示字符串的長度;10000000 aaaaaaaa bbbbbbbb cccccccc dddddddd
特大字符串,須要使用額外 4 個字節來表示長度。第一個字節前綴是10
,剩餘 6 位沒有使用,統一置爲零;11000000
表示 int16;11010000
表示 int32;11100000
表示 int64;11110000
表示 int24;11111110
表示 int8;11111111
表示 ziplist 的結束,也就是 zlend 的值 0xFF;1111xxxx
表示極小整數,xxxx 的範圍只能是 (0001~1101
), 也就是1~13
。若是是第10種狀況,那麼entry的構成就發生變化了:
<prevlen> <encoding>
由於數據已經存儲在encoding字段中了。
能夠看出Redis根據encoding字段的前兩位來判斷存儲的數據是字符串(字節數組)仍是整型,若是是字符串,還能夠經過encoding字段的前兩位來判斷字符串的長度;若是是整形,則要經過後面的位來判斷具體長度。
咱們上面說了那麼多關於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佔用的內存就太大了。
Redis並無像上篇博客介紹的SDS同樣,封裝一個結構體來保存ziplist,而是經過定義一系列宏來對數據進行操做,也就是說ziplist是一堆字節數據,上面所說的ziplist的佈局和ziplist中的entry的佈局只是抽象出來的概念。
在文章比較前面的部分,咱們作了實驗來證實,知足必定的條件後,zset、hash的底層存儲結構再也不是ziplist,既然ziplist那麼牛逼,Redis的開發者也花了那麼多精力在ziplist的設計上面,爲何zset、hash的底層存儲結構不能一直是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元素的個數只能遍歷了。
看到了吧,Redis真不是想象中的那麼簡單,須要研究的東西仍是挺多,也挺複雜的,若是咱們不去學習,可能以爲本身徹底掌握了Redis,可是一旦開始學習了,才發現咱們先前掌握的只是皮毛。驗證了一句話,知道的越多,不知道的越多。
本篇博客到這裏就結束了。