本篇文章為大家展示了redis中內(nèi)部數(shù)據(jù)結(jié)構(gòu)ziplist的作用是什么,內(nèi)容簡(jiǎn)明扼要并且容易理解,絕對(duì)能使你眼前一亮,通過(guò)這篇文章的詳細(xì)介紹希望你能有所收獲。
成都創(chuàng)新互聯(lián)專注于石城企業(yè)網(wǎng)站建設(shè),響應(yīng)式網(wǎng)站設(shè)計(jì),電子商務(wù)商城網(wǎng)站建設(shè)。石城網(wǎng)站建設(shè)公司,為石城等地區(qū)提供建站服務(wù)。全流程按需網(wǎng)站制作,專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,成都創(chuàng)新互聯(lián)專業(yè)和態(tài)度為您提供的服務(wù)
什么是ziplist
Redis官方對(duì)于ziplist的定義是(出自ziplist.c的文件頭部注釋):
The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and integer values, where integers are encoded as actual integers instead of a series of characters. It allows push and pop operations on either side of the list in O(1) time.
翻譯一下就是說(shuō):ziplist是一個(gè)經(jīng)過(guò)特殊編碼的雙向鏈表,它的設(shè)計(jì)目標(biāo)就是為了提高存儲(chǔ)效率。ziplist可以用于存儲(chǔ)字符串或整數(shù),其中整數(shù)是按真正的二進(jìn)制表示進(jìn)行編碼的,而不是編碼成字符串序列。它能以O(shè)(1)的時(shí)間復(fù)雜度在表的兩端提供push
和pop
操作。
實(shí)際上,ziplist充分體現(xiàn)了Redis對(duì)于存儲(chǔ)效率的追求。一個(gè)普通的雙向鏈表,鏈表中每一項(xiàng)都占用獨(dú)立的一塊內(nèi)存,各項(xiàng)之間用地址指針(或引用)連接起來(lái)。這種方式會(huì)帶來(lái)大量的內(nèi)存碎片,而且地址指針也會(huì)占用額外的內(nèi)存。而ziplist卻是將表中每一項(xiàng)存放在前后連續(xù)的地址空間內(nèi),一個(gè)ziplist整體占用一大塊內(nèi)存。它是一個(gè)表(list),但其實(shí)不是一個(gè)鏈表(linked list)。
另外,ziplist為了在細(xì)節(jié)上節(jié)省內(nèi)存,對(duì)于值的存儲(chǔ)采用了變長(zhǎng)的編碼方式,大概意思是說(shuō),對(duì)于大的整數(shù),就多用一些字節(jié)來(lái)存儲(chǔ),而對(duì)于小的整數(shù),就少用一些字節(jié)來(lái)存儲(chǔ)。我們接下來(lái)很快就會(huì)討論到這些實(shí)現(xiàn)細(xì)節(jié)。
ziplist的數(shù)據(jù)結(jié)構(gòu)組成是本文要討論的重點(diǎn)。實(shí)際上,ziplist還是稍微有點(diǎn)復(fù)雜的,它復(fù)雜的地方就在于它的數(shù)據(jù)結(jié)構(gòu)定義。一旦理解了數(shù)據(jù)結(jié)構(gòu),它的一些操作也就比較容易理解了。
我們接下來(lái)先從總體上介紹一下ziplist的數(shù)據(jù)結(jié)構(gòu)定義,然后舉一個(gè)實(shí)際的例子,通過(guò)例子來(lái)解釋ziplist的構(gòu)成。如果你看懂了這一部分,本文的任務(wù)就算完成了一大半了。
從宏觀上看,ziplist的內(nèi)存結(jié)構(gòu)如下:
各個(gè)部分在內(nèi)存上是前后相鄰的,它們分別的含義如下:
: 32bit,表示ziplist占用的字節(jié)總數(shù)(也包括
本身占用的4個(gè)字節(jié))。
: 32bit,表示ziplist表中最后一項(xiàng)(entry)在ziplist中的偏移字節(jié)數(shù)。
的存在,使得我們可以很方便地找到最后一項(xiàng)(不用遍歷整個(gè)ziplist),從而可以在ziplist尾端快速地執(zhí)行push或pop操作。
: 16bit, 表示ziplist中數(shù)據(jù)項(xiàng)(entry)的個(gè)數(shù)。zllen字段因?yàn)橹挥?6bit,所以可以表達(dá)的最大值為2^16-1。這里需要特別注意的是,如果ziplist中數(shù)據(jù)項(xiàng)個(gè)數(shù)超過(guò)了16bit能表達(dá)的最大值,ziplist仍然可以來(lái)表示。那怎么表示呢?這里做了這樣的規(guī)定:如果
小于等于2^16-2(也就是不等于2^16-1),那么
就表示ziplist中數(shù)據(jù)項(xiàng)的個(gè)數(shù);否則,也就是
等于16bit全為1的情況,那么
就不表示數(shù)據(jù)項(xiàng)個(gè)數(shù)了,這時(shí)候要想知道ziplist中數(shù)據(jù)項(xiàng)總數(shù),那么必須對(duì)ziplist從頭到尾遍歷各個(gè)數(shù)據(jù)項(xiàng),才能計(jì)數(shù)出來(lái)。
: 表示真正存放數(shù)據(jù)的數(shù)據(jù)項(xiàng),長(zhǎng)度不定。一個(gè)數(shù)據(jù)項(xiàng)(entry)也有它自己的內(nèi)部結(jié)構(gòu),這個(gè)稍后再解釋。
: ziplist最后1個(gè)字節(jié),是一個(gè)結(jié)束標(biāo)記,值固定等于255。
上面的定義中還值得注意的一點(diǎn)是:
,
,
既然占據(jù)多個(gè)字節(jié),那么在存儲(chǔ)的時(shí)候就有大端(big endian)和小端(little endian)的區(qū)別。ziplist采取的是小端模式來(lái)存儲(chǔ),這在下面我們介紹具體例子的時(shí)候還會(huì)再詳細(xì)解釋。
我們?cè)賮?lái)看一下每一個(gè)數(shù)據(jù)項(xiàng)
的構(gòu)成:
我們看到在真正的數(shù)據(jù)()前面,還有兩個(gè)字段:
: 表示前一個(gè)數(shù)據(jù)項(xiàng)占用的總字節(jié)數(shù)。這個(gè)字段的用處是為了讓ziplist能夠從后向前遍歷(從后一項(xiàng)的位置,只需向前偏移prevrawlen個(gè)字節(jié),就找到了前一項(xiàng))。這個(gè)字段采用變長(zhǎng)編碼。
: 表示當(dāng)前數(shù)據(jù)項(xiàng)的數(shù)據(jù)長(zhǎng)度(即部分的長(zhǎng)度)。也采用變長(zhǎng)編碼。
那么
和
是怎么進(jìn)行變長(zhǎng)編碼的呢?各位讀者打起精神了,我們終于講到了ziplist的定義中最繁瑣的地方了。
先說(shuō)
。它有兩種可能,或者是1個(gè)字節(jié),或者是5個(gè)字節(jié):
如果前一個(gè)數(shù)據(jù)項(xiàng)占用字節(jié)數(shù)小于254,那么
就只用一個(gè)字節(jié)來(lái)表示,這個(gè)字節(jié)的值就是前一個(gè)數(shù)據(jù)項(xiàng)的占用字節(jié)數(shù)。
如果前一個(gè)數(shù)據(jù)項(xiàng)占用字節(jié)數(shù)大于等于254,那么
就用5個(gè)字節(jié)來(lái)表示,其中第1個(gè)字節(jié)的值是254(作為這種情況的一個(gè)標(biāo)記),而后面4個(gè)字節(jié)組成一個(gè)整型值,來(lái)真正存儲(chǔ)前一個(gè)數(shù)據(jù)項(xiàng)的占用字節(jié)數(shù)。
有人會(huì)問(wèn)了,為什么沒(méi)有255的情況呢?
這是因?yàn)椋?55已經(jīng)定義為ziplist結(jié)束標(biāo)記
的值了。在ziplist的很多操作的實(shí)現(xiàn)中,都會(huì)根據(jù)數(shù)據(jù)項(xiàng)的第1個(gè)字節(jié)是不是255來(lái)判斷當(dāng)前是不是到達(dá)ziplist的結(jié)尾了,因此一個(gè)正常的數(shù)據(jù)的第1個(gè)字節(jié)(也就是
的第1個(gè)字節(jié))是不能夠取255這個(gè)值的,否則就沖突了。
而
字段就更加復(fù)雜了,它根據(jù)第1個(gè)字節(jié)的不同,總共分為9種情況(下面的表示法是按二進(jìn)制表示):
|00pppppp| - 1 byte。第1個(gè)字節(jié)最高兩個(gè)bit是00,那么
字段只有1個(gè)字節(jié),剩余的6個(gè)bit用來(lái)表示長(zhǎng)度值,最高可以表示63 (2^6-1)。
|01pppppp|qqqqqqqq| - 2 bytes。第1個(gè)字節(jié)最高兩個(gè)bit是01,那么
字段占2個(gè)字節(jié),總共有14個(gè)bit用來(lái)表示長(zhǎng)度值,最高可以表示16383 (2^14-1)。
|10__|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes。第1個(gè)字節(jié)最高兩個(gè)bit是10,那么len字段占5個(gè)字節(jié),總共使用32個(gè)bit來(lái)表示長(zhǎng)度值(6個(gè)bit舍棄不用),最高可以表示2^32-1。需要注意的是:在前三種情況下,都是按字符串來(lái)存儲(chǔ)的;從下面第4種情況開始,
開始變?yōu)榘凑麛?shù)來(lái)存儲(chǔ)了。
|11000000| - 1 byte。
字段占用1個(gè)字節(jié),值為0xC0,后面的數(shù)據(jù)存儲(chǔ)為2個(gè)字節(jié)的int16_t類型。
|11010000| - 1 byte。
字段占用1個(gè)字節(jié),值為0xD0,后面的數(shù)據(jù)存儲(chǔ)為4個(gè)字節(jié)的int32_t類型。
|11100000| - 1 byte。
字段占用1個(gè)字節(jié),值為0xE0,后面的數(shù)據(jù)存儲(chǔ)為8個(gè)字節(jié)的int64_t類型。
|11110000| - 1 byte。
字段占用1個(gè)字節(jié),值為0xF0,后面的數(shù)據(jù)存儲(chǔ)為3個(gè)字節(jié)長(zhǎng)的整數(shù)。
|11111110| - 1 byte。
字段占用1個(gè)字節(jié),值為0xFE,后面的數(shù)據(jù)存儲(chǔ)為1個(gè)字節(jié)的整數(shù)。
|1111xxxx| - - (xxxx的值在0001和1101之間)。這是一種特殊情況,xxxx從1到13一共13個(gè)值,這時(shí)就用這13個(gè)值來(lái)表示真正的數(shù)據(jù)。注意,這里是表示真正的數(shù)據(jù),而不是數(shù)據(jù)長(zhǎng)度了。也就是說(shuō),在這種情況下,后面不再需要一個(gè)單獨(dú)的字段來(lái)表示真正的數(shù)據(jù)了,而是
和合二為一了。另外,由于xxxx只能取0001和1101這13個(gè)值了(其它可能的值和其它情況沖突了,比如0000和1110分別同前面第7種第8種情況沖突,1111跟結(jié)束標(biāo)記沖突),而小數(shù)值應(yīng)該從0開始,因此這13個(gè)值分別表示0到12,即xxxx的值減去1才是它所要表示的那個(gè)整數(shù)數(shù)據(jù)的值。
好了,ziplist的數(shù)據(jù)結(jié)構(gòu)定義,我們介紹完了,現(xiàn)在我們看一個(gè)具體的例子。
上圖是一份真實(shí)的ziplist數(shù)據(jù)。我們逐項(xiàng)解讀一下:
這個(gè)ziplist一共包含33個(gè)字節(jié)。字節(jié)編號(hào)從byte[0]到byte[32]。圖中每個(gè)字節(jié)的值使用16進(jìn)制表示。
頭4個(gè)字節(jié)(0x21000000)是按小端(little endian)模式存儲(chǔ)的
字段。什么是小端呢?就是指數(shù)據(jù)的低字節(jié)保存在內(nèi)存的低地址中(參見維基百科詞條
Endianness)。因此,這里
的值應(yīng)該解析成0x00000021,用十進(jìn)制表示正好就是33。
接下來(lái)4個(gè)字節(jié)(byte[4..7])是
,用小端存儲(chǔ)模式來(lái)解釋,它的值是0x0000001D(值為29),表示最后一個(gè)數(shù)據(jù)項(xiàng)在byte[29]的位置(那個(gè)數(shù)據(jù)項(xiàng)為0x05FE14)。
再接下來(lái)2個(gè)字節(jié)(byte[8..9]),值為0x0004,表示這個(gè)ziplist里一共存有4項(xiàng)數(shù)據(jù)。
接下來(lái)6個(gè)字節(jié)(byte[10..15])是第1個(gè)數(shù)據(jù)項(xiàng)。其中,prevrawlen=0,因?yàn)樗懊鏇](méi)有數(shù)據(jù)項(xiàng);len=4,相當(dāng)于前面定義的9種情況中的第1種,表示后面4個(gè)字節(jié)按字符串存儲(chǔ)數(shù)據(jù),數(shù)據(jù)的值為”name”。
接下來(lái)8個(gè)字節(jié)(byte[16..23])是第2個(gè)數(shù)據(jù)項(xiàng),與前面數(shù)據(jù)項(xiàng)存儲(chǔ)格式類似,存儲(chǔ)1個(gè)字符串”tielei”。
接下來(lái)5個(gè)字節(jié)(byte[24..28])是第3個(gè)數(shù)據(jù)項(xiàng),與前面數(shù)據(jù)項(xiàng)存儲(chǔ)格式類似,存儲(chǔ)1個(gè)字符串”age”。
接下來(lái)3個(gè)字節(jié)(byte[29..31])是最后一個(gè)數(shù)據(jù)項(xiàng),它的格式與前面的數(shù)據(jù)項(xiàng)存儲(chǔ)格式不太一樣。其中,第1個(gè)字節(jié)prevrawlen=5,表示前一個(gè)數(shù)據(jù)項(xiàng)占用5個(gè)字節(jié);第2個(gè)字節(jié)=FE,相當(dāng)于前面定義的9種情況中的第8種,所以后面還有1個(gè)字節(jié)用來(lái)表示真正的數(shù)據(jù),并且以整數(shù)表示。它的值是20(0x14)。
最后1個(gè)字節(jié)(byte[32])表示
,是固定的值255(0xFF)。
總結(jié)一下,這個(gè)ziplist里存了4個(gè)數(shù)據(jù)項(xiàng),分別為:
字符串: “name”
字符串: “tielei”
字符串: “age”
整數(shù): 20
(好吧,被你發(fā)現(xiàn)了~~tielei實(shí)際上當(dāng)然不是20歲,他哪有那么年輕啊……)
實(shí)際上,這個(gè)ziplist是通過(guò)兩個(gè)hset
命令創(chuàng)建出來(lái)的。這個(gè)我們后半部分會(huì)再提到。
好了,既然你已經(jīng)閱讀到這里了,說(shuō)明你還是很有耐心的(其實(shí)我寫到這里也已經(jīng)累得不行了)。可以先把本文收藏,休息一下,回頭再看后半部分。
接下來(lái)我要貼一些代碼了。
我們先不著急看實(shí)現(xiàn),先來(lái)挑幾個(gè)ziplist的重要的接口,看看它們長(zhǎng)什么樣子:
unsigned char *ziplistNew(void); unsigned char *ziplistMerge(unsigned char **first, unsigned char **second); unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where); unsigned char *ziplistIndex(unsigned char *zl, int index); unsigned char *ziplistNext(unsigned char *zl, unsigned char *p); unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p); unsigned char *ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen); unsigned char *ziplistDelete(unsigned char *zl, unsigned char **p); unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip); unsigned int ziplistLen(unsigned char *zl);
我們從這些接口的名字就可以粗略猜出它們的功能,下面簡(jiǎn)單解釋一下:
ziplist的數(shù)據(jù)類型,沒(méi)有用自定義的struct之類的來(lái)表達(dá),而就是簡(jiǎn)單的unsigned char *。這是因?yàn)閦iplist本質(zhì)上就是一塊連續(xù)內(nèi)存,內(nèi)部組成結(jié)構(gòu)又是一個(gè)高度動(dòng)態(tài)的設(shè)計(jì)(變長(zhǎng)編碼),也沒(méi)法用一個(gè)固定的數(shù)據(jù)結(jié)構(gòu)來(lái)表達(dá)。
ziplistNew: 創(chuàng)建一個(gè)空的ziplist(只包含
)。
ziplistMerge: 將兩個(gè)ziplist合并成一個(gè)新的ziplist。
ziplistPush: 在ziplist的頭部或尾端插入一段數(shù)據(jù)(產(chǎn)生一個(gè)新的數(shù)據(jù)項(xiàng))。注意一下這個(gè)接口的返回值,是一個(gè)新的ziplist。調(diào)用方必須用這里返回的新的ziplist,替換之前傳進(jìn)來(lái)的舊的ziplist變量,而經(jīng)過(guò)這個(gè)函數(shù)處理之后,原來(lái)舊的ziplist變量就失效了。為什么一個(gè)簡(jiǎn)單的插入操作會(huì)導(dǎo)致產(chǎn)生一個(gè)新的ziplist呢?這是因?yàn)閦iplist是一塊連續(xù)空間,對(duì)它的追加操作,會(huì)引發(fā)內(nèi)存的realloc,因此ziplist的內(nèi)存位置可能會(huì)發(fā)生變化。實(shí)際上,我們?cè)谥敖榻Bsds的文章中提到過(guò)類似這種接口使用模式(參見sdscatlen函數(shù)的說(shuō)明)。
ziplistIndex: 返回index參數(shù)指定的數(shù)據(jù)項(xiàng)的內(nèi)存位置。index可以是負(fù)數(shù),表示從尾端向前進(jìn)行索引。
ziplistNext和ziplistPrev分別返回一個(gè)ziplist中指定數(shù)據(jù)項(xiàng)p的后一項(xiàng)和前一項(xiàng)。
ziplistInsert: 在ziplist的任意數(shù)據(jù)項(xiàng)前面插入一個(gè)新的數(shù)據(jù)項(xiàng)。
ziplistDelete: 刪除指定的數(shù)據(jù)項(xiàng)。
ziplistFind: 查找給定的數(shù)據(jù)(由vstr和vlen指定)。注意它有一個(gè)skip參數(shù),表示查找的時(shí)候每次比較之間要跳過(guò)幾個(gè)數(shù)據(jù)項(xiàng)。為什么會(huì)有這么一個(gè)參數(shù)呢?其實(shí)這個(gè)參數(shù)的主要用途是當(dāng)用ziplist表示hash結(jié)構(gòu)的時(shí)候,是按照一個(gè)field,一個(gè)value來(lái)依次存入ziplist的。也就是說(shuō),偶數(shù)索引的數(shù)據(jù)項(xiàng)存field,奇數(shù)索引的數(shù)據(jù)項(xiàng)存value。當(dāng)按照f(shuō)ield的值進(jìn)行查找的時(shí)候,就需要把奇數(shù)項(xiàng)跳過(guò)去。
ziplistLen: 計(jì)算ziplist的長(zhǎng)度(即包含數(shù)據(jù)項(xiàng)的個(gè)數(shù))。
ziplist的相關(guān)接口的具體實(shí)現(xiàn),還是有些復(fù)雜的,限于篇幅的原因,我們這里只結(jié)合代碼來(lái)講解插入的邏輯。插入是很有代表性的操作,通過(guò)這部分來(lái)一窺ziplist內(nèi)部的實(shí)現(xiàn),其它部分的實(shí)現(xiàn)我們也就會(huì)很容易理解了。
ziplistPush和ziplistInsert都是插入,只是對(duì)于插入位置的限定不同。它們?cè)趦?nèi)部實(shí)現(xiàn)都依賴一個(gè)名為__ziplistInsert的內(nèi)部函數(shù),其代碼如下(出自ziplist.c):
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) { size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen; unsigned int prevlensize, prevlen = 0; size_t offset; int nextdiff = 0; unsigned char encoding = 0; long long value = 123456789; /* initialized to avoid warning. Using a value that is easy to see if for some reason we use it uninitialized. */ zlentry tail; /* Find out prevlen for the entry that is inserted. */ if (p[0] != ZIP_END) { ZIP_DECODE_PREVLEN(p, prevlensize, prevlen); } else { unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl); if (ptail[0] != ZIP_END) { prevlen = zipRawEntryLength(ptail); } } /* See if the entry can be encoded */ if (zipTryEncoding(s,slen,&value,&encoding)) { /* 'encoding' is set to the appropriate integer encoding */ reqlen = zipIntSize(encoding); } else { /* 'encoding' is untouched, however zipEncodeLength will use the * string length to figure out how to encode it. */ reqlen = slen; } /* We need space for both the length of the previous entry and * the length of the payload. */ reqlen += zipPrevEncodeLength(NULL,prevlen); reqlen += zipEncodeLength(NULL,encoding,slen); /* When the insert position is not equal to the tail, we need to * make sure that the next entry can hold this entry's length in * its prevlen field. */ nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0; /* Store offset because a realloc may change the address of zl. */ offset = p-zl; zl = ziplistResize(zl,curlen+reqlen+nextdiff); p = zl+offset; /* Apply memory move when necessary and update tail offset. */ if (p[0] != ZIP_END) { /* Subtract one because of the ZIP_END bytes */ memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); /* Encode this entry's raw length in the next entry. */ zipPrevEncodeLength(p+reqlen,reqlen); /* Update offset for tail */ ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen); /* When the tail contains more than one entry, we need to take * "nextdiff" in account as well. Otherwise, a change in the * size of prevlen doesn't have an effect on the *tail* offset. */ zipEntry(p+reqlen, &tail); if (p[reqlen+tail.headersize+tail.len] != ZIP_END) { ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff); } } else { /* This element will be the new tail. */ ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl); } /* When nextdiff != 0, the raw length of the next entry has changed, so * we need to cascade the update throughout the ziplist */ if (nextdiff != 0) { offset = p-zl; zl = __ziplistCascadeUpdate(zl,p+reqlen); p = zl+offset; } /* Write the entry */ p += zipPrevEncodeLength(p,prevlen); p += zipEncodeLength(p,encoding,slen); if (ZIP_IS_STR(encoding)) { memcpy(p,s,slen); } else { zipSaveInteger(p,value,encoding); } ZIPLIST_INCR_LENGTH(zl,1); return zl; }
我們來(lái)簡(jiǎn)單解析一下這段代碼:
這個(gè)函數(shù)是在指定的位置p插入一段新的數(shù)據(jù),待插入數(shù)據(jù)的地址指針是s,長(zhǎng)度為slen。插入后形成一個(gè)新的數(shù)據(jù)項(xiàng),占據(jù)原來(lái)p的配置,原來(lái)位于p位置的數(shù)據(jù)項(xiàng)以及后面的所有數(shù)據(jù)項(xiàng),需要統(tǒng)一向后移動(dòng),給新插入的數(shù)據(jù)項(xiàng)留出空間。參數(shù)p指向的是ziplist中某一個(gè)數(shù)據(jù)項(xiàng)的起始位置,或者在向尾端插入的時(shí)候,它指向ziplist的結(jié)束標(biāo)記
。
函數(shù)開始先計(jì)算出待插入位置前一個(gè)數(shù)據(jù)項(xiàng)的長(zhǎng)度prevlen
。這個(gè)長(zhǎng)度要存入新插入的數(shù)據(jù)項(xiàng)的
字段。
然后計(jì)算當(dāng)前數(shù)據(jù)項(xiàng)占用的總字節(jié)數(shù)reqlen
,它包含三部分:
,
和真正的數(shù)據(jù)。其中的數(shù)據(jù)部分會(huì)通過(guò)調(diào)用zipTryEncoding
先來(lái)嘗試轉(zhuǎn)成整數(shù)。
由于插入導(dǎo)致的ziplist對(duì)于內(nèi)存的新增需求,除了待插入數(shù)據(jù)項(xiàng)占用的reqlen
之外,還要考慮原來(lái)p位置的數(shù)據(jù)項(xiàng)(現(xiàn)在要排在待插入數(shù)據(jù)項(xiàng)之后)的
字段的變化。本來(lái)它保存的是前一項(xiàng)的總長(zhǎng)度,現(xiàn)在變成了保存當(dāng)前插入的數(shù)據(jù)項(xiàng)的總長(zhǎng)度。這樣它的
字段本身需要的存儲(chǔ)空間也可能發(fā)生變化,這個(gè)變化可能是變大也可能是變小。這個(gè)變化了多少的值nextdiff
,是調(diào)用zipPrevLenByteDiff
計(jì)算出來(lái)的。如果變大了,nextdiff
是正值,否則是負(fù)值。
現(xiàn)在很容易算出來(lái)插入后新的ziplist需要多少字節(jié)了,然后調(diào)用ziplistResize
來(lái)重新調(diào)整大小。ziplistResize的實(shí)現(xiàn)里會(huì)調(diào)用allocator的zrealloc
,它有可能會(huì)造成數(shù)據(jù)拷貝。
現(xiàn)在額外的空間有了,接下來(lái)就是將原來(lái)p位置的數(shù)據(jù)項(xiàng)以及后面的所有數(shù)據(jù)都向后挪動(dòng),并為它設(shè)置新的
字段。此外,還可能需要調(diào)整ziplist的
字段。
最后,組裝新的待插入數(shù)據(jù)項(xiàng),放在位置p。
hash是Redis中可以用來(lái)存儲(chǔ)一個(gè)對(duì)象結(jié)構(gòu)的比較理想的數(shù)據(jù)類型。一個(gè)對(duì)象的各個(gè)屬性,正好對(duì)應(yīng)一個(gè)hash結(jié)構(gòu)的各個(gè)field。
我們?cè)诰W(wǎng)上很容易找到這樣一些技術(shù)文章,它們會(huì)說(shuō)存儲(chǔ)一個(gè)對(duì)象,使用hash比string要節(jié)省內(nèi)存。實(shí)際上這么說(shuō)是有前提的,具體取決于對(duì)象怎么來(lái)存儲(chǔ)。如果你把對(duì)象的多個(gè)屬性存儲(chǔ)到多個(gè)key上(各個(gè)屬性值存成string),當(dāng)然占的內(nèi)存要多。但如果你采用一些序列化方法,比如 Protocol Buffers,或者 Apache Thrift,先把對(duì)象序列化為字節(jié)數(shù)組,然后再存入到Redis的string中,那么跟hash相比,哪一種更省內(nèi)存,就不一定了。
當(dāng)然,hash比序列化后再存入string的方式,在支持的操作命令上,還是有優(yōu)勢(shì)的:它既支持多個(gè)field同時(shí)存?。?code>hmset/hmget
),也支持按照某個(gè)特定的field單獨(dú)存?。?code>hset/hget
)。
實(shí)際上,hash隨著數(shù)據(jù)的增大,其底層數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)是會(huì)發(fā)生變化的,當(dāng)然存儲(chǔ)效率也就不同。在field比較少,各個(gè)value值也比較小的時(shí)候,hash采用ziplist來(lái)實(shí)現(xiàn);而隨著field增多和value值增大,hash可能會(huì)變成dict來(lái)實(shí)現(xiàn)。當(dāng)hash底層變成dict來(lái)實(shí)現(xiàn)的時(shí)候,它的存儲(chǔ)效率就沒(méi)法跟那些序列化方式相比了。
當(dāng)我們?yōu)槟硞€(gè)key第一次執(zhí)行
hset key field value
命令的時(shí)候,Redis會(huì)創(chuàng)建一個(gè)hash結(jié)構(gòu),這個(gè)新創(chuàng)建的hash底層就是一個(gè)ziplist。
robj *createHashObject(void) { unsigned char *zl = ziplistNew(); robj *o = createObject(OBJ_HASH, zl); o->encoding = OBJ_ENCODING_ZIPLIST; return o; }
上面的createHashObject
函數(shù),出自object.c,它負(fù)責(zé)的任務(wù)就是創(chuàng)建一個(gè)新的hash結(jié)構(gòu)??梢钥闯?,它創(chuàng)建了一個(gè)type = OBJ_HASH
但encoding = OBJ_ENCODING_ZIPLIST
的robj對(duì)象。
實(shí)際上,本文前面給出的那個(gè)ziplist實(shí)例,就是由如下兩個(gè)命令構(gòu)建出來(lái)的。
hset user:100 name tielei hset user:100 age 20
每執(zhí)行一次hset
命令,插入的field和value分別作為一個(gè)新的數(shù)據(jù)項(xiàng)插入到ziplist中(即每次hset
產(chǎn)生兩個(gè)數(shù)據(jù)項(xiàng))。
當(dāng)隨著數(shù)據(jù)的插入,hash底層的這個(gè)ziplist就可能會(huì)轉(zhuǎn)成dict。那么到底插入多少才會(huì)轉(zhuǎn)呢?
還記得本文開頭提到的兩個(gè)Redis配置嗎?
hash-max-ziplist-entries 512 hash-max-ziplist-value 64
這個(gè)配置的意思是說(shuō),在如下兩個(gè)條件之一滿足的時(shí)候,ziplist會(huì)轉(zhuǎn)成dict:
當(dāng)hash中的數(shù)據(jù)項(xiàng)(即field-value對(duì))的數(shù)目超過(guò)512的時(shí)候,也就是ziplist數(shù)據(jù)項(xiàng)超過(guò)1024的時(shí)候(請(qǐng)參考t_hash.c中的hashTypeSet
函數(shù))。
當(dāng)hash中插入的任意一個(gè)value的長(zhǎng)度超過(guò)了64的時(shí)候(請(qǐng)參考t_hash.c中的hashTypeTryConversion
函數(shù))。
Redis的hash之所以這樣設(shè)計(jì),是因?yàn)楫?dāng)ziplist變得很大的時(shí)候,它有如下幾個(gè)缺點(diǎn):
每次插入或修改引發(fā)的realloc操作會(huì)有更大的概率造成內(nèi)存拷貝,從而降低性能。
一旦發(fā)生內(nèi)存拷貝,內(nèi)存拷貝的成本也相應(yīng)增加,因?yàn)橐截惛蟮囊粔K數(shù)據(jù)。
當(dāng)ziplist數(shù)據(jù)項(xiàng)過(guò)多的時(shí)候,在它上面查找指定的數(shù)據(jù)項(xiàng)就會(huì)性能變得很低,因?yàn)閦iplist上的查找需要進(jìn)行遍歷。
上述內(nèi)容就是Redis中內(nèi)部數(shù)據(jù)結(jié)構(gòu)ziplist的作用是什么,你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。