本篇部落格參考:
上篇部落格中,我給大家蜻蜓點水般的介紹了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佔用的位元組總數(包括
本身佔用的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來判斷儲存資料的型別的:
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的點點滴滴,下面將要說的內容可能會顛覆你三觀,我們在原始碼中可以看到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,但是一旦開始學習了,才發現我們先前掌握的只是皮毛。驗證了一句話,知道的越多,不知道的越多。
本篇部落格到這裡就結束了。