摘要:用不用作為分界是因為是的值,它用于判斷是否到達(dá)尾部。這種類型的節(jié)點的大小介于與之間,但是為了表示整數(shù),取出低四位之后會將其作為實際的值。當(dāng)小于字節(jié)時,節(jié)點存為上圖的第二種類型,高位為,后續(xù)位表示的長度。
// 文中引用的代碼來源于Redis3.2
前言Redis是基于內(nèi)存的nosql,有些場景下為了節(jié)省內(nèi)存redis會用“時間”換“空間”。
ziplist就是很典型的例子。
ziplist是list鍵、hash鍵以及zset鍵的底層實現(xiàn)之一(3.0之后list鍵已經(jīng)不直接用ziplist和linkedlist作為底層實現(xiàn)了,取而代之的是quicklist)
這些鍵的常規(guī)底層實現(xiàn)如下:
list鍵:雙向鏈表
hash鍵:字典dict
zset鍵:跳躍表zskiplist
但是當(dāng)list鍵里包含的元素較少、并且每個元素要么是小整數(shù)要么是長度較小的字符串時,redis將會用ziplist作為list鍵的底層實現(xiàn)。同理hash和zset在這種場景下也會使用ziplist。
既然已有底層結(jié)構(gòu)可以實現(xiàn)list、hash、zset鍵,為什么還要用ziplist呢?
當(dāng)然是為了節(jié)省內(nèi)存空間
我們先來看看ziplist是如何壓縮的
ziplist是由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序存儲結(jié)構(gòu),類似于數(shù)組,ziplist在內(nèi)存中是連續(xù)存儲的,但是不同于數(shù)組,為了節(jié)省內(nèi)存 ziplist的每個元素所占的內(nèi)存大小可以不同(數(shù)組中叫元素,ziplist叫節(jié)點entry,下文都用“節(jié)點”),每個節(jié)點可以用來存儲一個整數(shù)或者一個字符串。
下圖是ziplist在內(nèi)存中的布局
zlbytes: ziplist的長度(單位: 字節(jié)),是一個32位無符號整數(shù)
zltail: ziplist最后一個節(jié)點的偏移量,反向遍歷ziplist或者pop尾部節(jié)點的時候有用。
zllen: ziplist的節(jié)點(entry)個數(shù)
entry: 節(jié)點
zlend: 值為0xFF,用于標(biāo)記ziplist的結(jié)尾
普通數(shù)組的遍歷是根據(jù)數(shù)組里存儲的數(shù)據(jù)類型 找到下一個元素的,例如int類型的數(shù)組訪問下一個元素時每次只需要移動一個sizeof(int)就行(實際上開發(fā)者只需讓指針p+1就行,在這里引入sizeof(int)只是為了說明區(qū)別)。
上文說了,ziplist的每個節(jié)點的長度是可以不一樣的,而我們面對不同長度的節(jié)點又不可能直接sizeof(entry),那么它是怎么訪問下一個節(jié)點呢?
ziplist將一些必要的偏移量信息記錄在了每一個節(jié)點里,使之能跳到上一個節(jié)點或下一個節(jié)點。
接下來我們看看節(jié)點的布局
每個節(jié)點由三部分組成:prevlength、encoding、data
prevlengh: 記錄上一個節(jié)點的長度,為了方便反向遍歷ziplist
encoding: 當(dāng)前節(jié)點的編碼規(guī)則,下文會詳細(xì)說
data: 當(dāng)前節(jié)點的值,可以是數(shù)字或字符串
為了節(jié)省內(nèi)存,根據(jù)上一個節(jié)點的長度prevlength 可以將ziplist節(jié)點分為兩類:
entry的前8位小于254,則這8位就表示上一個節(jié)點的長度
entry的前8位等于254,則意味著上一個節(jié)點的長度無法用8位表示,后面32位才是真實的prevlength。用254 不用255(11111111)作為分界是因為255是zlend的值,它用于判斷ziplist是否到達(dá)尾部。
根據(jù)當(dāng)前節(jié)點存儲的數(shù)據(jù)類型及長度,可以將ziplist節(jié)點分為9類:
其中整數(shù)節(jié)點分為6類:
整數(shù)節(jié)點的encoding的長度為8位,其中高2位用來區(qū)分整數(shù)節(jié)點和字符串節(jié)點(高2位為11時是整數(shù)節(jié)點),低6位用來區(qū)分整數(shù)節(jié)點的類型,定義如下:
#define ZIP_INT_16B (0xc0 | 0<<4)//整數(shù)data,占16位(2字節(jié)) #define ZIP_INT_32B (0xc0 | 1<<4)//整數(shù)data,占32位(4字節(jié)) #define ZIP_INT_64B (0xc0 | 2<<4)//整數(shù)data,占64位(8字節(jié)) #define ZIP_INT_24B (0xc0 | 3<<4)//整數(shù)data,占24位(3字節(jié)) #define ZIP_INT_8B 0xfe //整數(shù)data,占8位(1字節(jié)) /* 4 bit integer immediate encoding */ //整數(shù)值1~13的節(jié)點沒有data,encoding的低四位用來表示data #define ZIP_INT_IMM_MASK 0x0f #define ZIP_INT_IMM_MIN 0xf1 /* 11110001 */ #define ZIP_INT_IMM_MAX 0xfd /* 11111101 */
值得注意的是 最后一種encoding是存儲整數(shù)0~12的節(jié)點的encoding,它沒有額外的data部分,encoding的高4位表示這個類型,低4位就是它的data。這種類型的節(jié)點的encoding大小介于ZIP_INT_24B與ZIP_INT_8B之間(1~13),但是為了表示整數(shù)0,取出低四位xxxx之后會將其-1作為實際的data值(0~12)。在函數(shù)zipLoadInteger中,我們可以看到這種類型節(jié)點的取值方法:
... } else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) { ret = (encoding & ZIP_INT_IMM_MASK)-1; } ...
字符串節(jié)點分為3類:
當(dāng)data小于63字節(jié)時(2^6),節(jié)點存為上圖的第一種類型,高2位為00,低6位表示data的長度。
當(dāng)data小于16383字節(jié)時(2^14),節(jié)點存為上圖的第二種類型,高2位為01,后續(xù)14位表示data的長度。
當(dāng)data小于4294967296字節(jié)時(2^32),節(jié)點存為上圖的第二種類型,高2位為10,下一字節(jié)起連續(xù)32位表示data的長度。
上圖可以看出:
不同于整數(shù)節(jié)點encoding永遠(yuǎn)是8位,字符串節(jié)點的encoding可以有8位、16位、40位三種長度
相同encoding類型的整數(shù)節(jié)點 data長度是固定的,但是相同encoding類型的字符串節(jié)點,data長度取決于encoding后半部分的值。
#define ZIP_STR_06B (0 << 6)//字符串data,最多有2^6字節(jié)(encoding后半部分的length有6位,length決定data有多少字節(jié)) #define ZIP_STR_14B (1 << 6)//字符串data,最多有2^14字節(jié) #define ZIP_STR_32B (2 << 6)//字符串data,最多有2^32字節(jié)
上文介紹了ziplist節(jié)點(entry)的分類,知道了節(jié)點可以細(xì)分為9種類型,那么當(dāng)遍歷一個ziplist時,指針到達(dá)某個節(jié)點時 如何判斷出節(jié)點的類型從而找到data呢?
已知節(jié)點的位置,求data的值根據(jù)圖2 entry布局 可以看出,若要算出data的偏移量,得先計算出prevlength所占內(nèi)存大?。?字節(jié)和5字節(jié)):
//根據(jù)ptr指向的entry,返回這個entry的prevlensize #define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do { if ((ptr)[0] < ZIP_BIGLEN) { (prevlensize) = 1; } else { (prevlensize) = 5; } } while(0);
接著再用ZIP_DECODE_LENGTH(ptr + prevlensize, encoding, lensize, len)算出encoding所占的字節(jié),返回給lensize;data所占的字節(jié)返回給len
//根據(jù)ptr指向的entry求出該entry的len(encoding里存的 data所占字節(jié))和lensize(encoding所占的字節(jié)) #define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do { ZIP_ENTRY_ENCODING((ptr), (encoding)); if ((encoding) < ZIP_STR_MASK) { if ((encoding) == ZIP_STR_06B) { (lensize) = 1; (len) = (ptr)[0] & 0x3f; } else if ((encoding) == ZIP_STR_14B) { (lensize) = 2; (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; } else if (encoding == ZIP_STR_32B) { (lensize) = 5; (len) = ((ptr)[1] << 24) | ((ptr)[2] << 16) | ((ptr)[3] << 8) | ((ptr)[4]); } else { assert(NULL); } } else { (lensize) = 1; (len) = zipIntSize(encoding); } } while(0); //將ptr的encoding解析成1個字節(jié):00000000、01000000、10000000(字符串類型)和11??????(整數(shù)類型) //如果是整數(shù)類型,encoding直接照抄ptr的;如果是字符串類型,encoding被截斷成一個字節(jié)并清零后6位 #define ZIP_ENTRY_ENCODING(ptr, encoding) do { (encoding) = (ptr[0]); if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; } while(0) //根據(jù)encoding返回數(shù)據(jù)(整數(shù))所占字節(jié)數(shù) unsigned int zipIntSize(unsigned char encoding) { switch(encoding) { case ZIP_INT_8B: return 1; case ZIP_INT_16B: return 2; case ZIP_INT_24B: return 3; case ZIP_INT_32B: return 4; case ZIP_INT_64B: return 8; default: return 0; /* 4 bit immediate */ } assert(NULL); return 0; }
完成以上步驟之后,即可算出data的位置:ptr+prevlensize+lensize,以及data的長度len
ziplist接口上文已經(jīng)闡述了ziplist的底層內(nèi)存布局,接下來看看一些基本的增刪改查操作在ziplist中是如何執(zhí)行的。
ziplistNew 創(chuàng)建一個ziplist O(1)/* Create a new empty ziplist. */ unsigned char *ziplistNew(void) { unsigned int bytes = ZIPLIST_HEADER_SIZE+1;//4字節(jié) 4字節(jié) 2字節(jié) 1字節(jié),沒有entry節(jié)點 unsigned char *zl = zmalloc(bytes); ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);// 賦值 ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);// ZIPLIST_LENGTH(zl) = 0;// zl[bytes-1] = ZIP_END;// return zl; } #define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))//空ziplist除了 的大小 #define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))// 的指針的值,可讀可寫 #define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))// 的指針的值 #define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))//空ziplist除了 的大小 #define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))// 的指針的值
參照著圖1理解會直觀些,分配了一塊內(nèi)存并初始化
//返回p節(jié)點之后data與vstr(長度是vlen)相等的節(jié)點,只找p節(jié)點之后每隔skip的節(jié)點 //時間復(fù)雜度 O(n) unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) { int skipcnt = 0; unsigned char vencoding = 0; long long vll = 0; while (p[0] != ZIP_END) { unsigned int prevlensize, encoding, lensize, len; unsigned char *q; ZIP_DECODE_PREVLENSIZE(p, prevlensize); ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len); q = p + prevlensize + lensize;//當(dāng)前節(jié)點的data if (skipcnt == 0) { /* Compare current entry with specified entry */ if (ZIP_IS_STR(encoding)) {//判斷當(dāng)前節(jié)點是不是字符串節(jié)點 if (len == vlen && memcmp(q, vstr, vlen) == 0) { return p; } } else { /* Find out if the searched field can be encoded. Note that * we do it only the first time, once done vencoding is set * to non-zero and vll is set to the integer value. */ if (vencoding == 0) {//這個代碼塊只會執(zhí)行一次,計算vstr的整數(shù)表示 if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) { //將參數(shù)給的節(jié)點vstr當(dāng)做整數(shù)節(jié)點轉(zhuǎn)換;將data值返回給vll,節(jié)點編碼返回給vencoding //進(jìn)入這個代碼塊說明將vstr轉(zhuǎn)換成整數(shù)失敗,vencoding不變,下次判斷當(dāng)前節(jié)點是整數(shù)節(jié)點之后可以跳過這個節(jié)點 /* If the entry can"t be encoded we set it to * UCHAR_MAX so that we don"t retry again the next * time. */ vencoding = UCHAR_MAX;//當(dāng)前節(jié)點是整數(shù)節(jié)點,但是vstr是字符串節(jié)點,跳過不用比較了 } /* Must be non-zero by now */ assert(vencoding); } /* Compare current entry with specified entry, do it only * if vencoding != UCHAR_MAX because if there is no encoding * possible for the field it can"t be a valid integer. */ if (vencoding != UCHAR_MAX) { long long ll = zipLoadInteger(q, encoding);//算出當(dāng)前節(jié)點的data if (ll == vll) { return p; } } } /* Reset skip count */ skipcnt = skip; } else { /* Skip entry */ skipcnt--; } /* Move to next entry */ p = q + len; } return NULL; } //嘗試將entry地址的內(nèi)容轉(zhuǎn)換成整數(shù),并根據(jù)這個整數(shù)算出一個合適的encoding返回給encoding參數(shù)。 //若無法轉(zhuǎn)換成整數(shù),則encoding不變,返回0,等到下次調(diào)用zipEncodeLength時再計算一個該字符串的encoding int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) { long long value; if (entrylen >= 32 || entrylen == 0) return 0; if (string2ll((char*)entry,entrylen,&value)) { /* Great, the string can be encoded. Check what"s the smallest * of our encoding types that can hold this value. */ if (value >= 0 && value <= 12) { *encoding = ZIP_INT_IMM_MIN+value; } else if (value >= INT8_MIN && value <= INT8_MAX) { *encoding = ZIP_INT_8B; } else if (value >= INT16_MIN && value <= INT16_MAX) { *encoding = ZIP_INT_16B; } else if (value >= INT24_MIN && value <= INT24_MAX) { *encoding = ZIP_INT_24B; } else if (value >= INT32_MIN && value <= INT32_MAX) { *encoding = ZIP_INT_32B; } else { *encoding = ZIP_INT_64B; } *v = value; return 1; } return 0; } /* Read integer encoded as "encoding" from "p" */ int64_t zipLoadInteger(unsigned char *p, unsigned char encoding) { int16_t i16; int32_t i32; int64_t i64, ret = 0; if (encoding == ZIP_INT_8B) { ret = ((int8_t*)p)[0]; } else if (encoding == ZIP_INT_16B) { memcpy(&i16,p,sizeof(i16)); memrev16ifbe(&i16); ret = i16; } else if (encoding == ZIP_INT_32B) { memcpy(&i32,p,sizeof(i32)); memrev32ifbe(&i32); ret = i32; } else if (encoding == ZIP_INT_24B) { i32 = 0; memcpy(((uint8_t*)&i32)+1,p,sizeof(i32)-sizeof(uint8_t)); memrev32ifbe(&i32); ret = i32>>8; } else if (encoding == ZIP_INT_64B) { memcpy(&i64,p,sizeof(i64)); memrev64ifbe(&i64); ret = i64; } else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) { ret = (encoding & ZIP_INT_IMM_MASK)-1; } else { assert(NULL); } return ret; }其他接口
ziplistInsert 往ziplist里插入一個entry 時間復(fù)雜度 平均:O(n), 最壞:O(n2)
ziplistDelete 從siplist里刪除一個entry 時間復(fù)雜度 平均:O(n), 最壞:O(n2)
為什么插入節(jié)點和刪除節(jié)點兩個接口的最壞時間復(fù)雜度會是O(n2)呢?這是由于ziplist的“連鎖更新”導(dǎo)致的,連鎖更新在最壞情況下需要對ziplist執(zhí)行n次空間重分配操作,而且每次空間重分配的最壞時間復(fù)雜度為O(n) ----《Redis設(shè)計與實現(xiàn)》
但是出現(xiàn)“連鎖更新”的情況并不多見,所以這里基本不會造成性能問題。
篇幅有限這里不能細(xì)說連鎖更新,感興趣可以閱讀《Redis設(shè)計與實現(xiàn)》的相關(guān)章節(jié)以及ziplist.c里的__ziplistCascadeUpdate()函數(shù)。
ziplist是為節(jié)省內(nèi)存空間而生的。
ziplist是一個為Redis專門提供的底層數(shù)據(jù)結(jié)構(gòu)之一,本身可以有序也可以無序。當(dāng)作為list和hash的底層實現(xiàn)時,節(jié)點之間沒有順序;當(dāng)作為zset的底層實現(xiàn)時,節(jié)點之間會按照大小順序排列。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/29617.html
摘要:記錄壓縮列表總共占用的字節(jié)數(shù),在對壓縮列表進(jìn)行內(nèi)存重分配時使用個字節(jié)。為十六進(jìn)制值,標(biāo)記一個壓縮列表的末尾具體的數(shù)據(jù)存放在中。占用或字節(jié)表示當(dāng)前存儲數(shù)據(jù)的類型與數(shù)據(jù)的長度。在學(xué)習(xí)的同時,如果沒有經(jīng)過自己的思考,收效甚微。 baiyan全部視頻:https://segmentfault.com/a/11... 為什么需要ziplist 乍一看標(biāo)題,我們可能還不知道ziplist是何許人也...
摘要:哈希對象哈希對象的可選編碼分別是和。編碼的哈希對象編碼的哈希對象使用壓縮列表作為底層實現(xiàn)。關(guān)于哈希編碼轉(zhuǎn)換的函數(shù),可以參考,源碼如下是原始對象,是目標(biāo)編碼。對應(yīng)源碼如下對象元素數(shù)量為,或者總結(jié)哈希對象有和編碼。 繼續(xù)擼我們的對象和數(shù)據(jù)類型。 上節(jié)我們一起認(rèn)識了字符串和列表,接下來還有哈希、集合和有序集合。 1 哈希對象 哈希對象的可選編碼分別是:ziplist 和 hashtable。...
閱讀 1288·2023-04-25 20:31
閱讀 3780·2021-10-14 09:42
閱讀 1563·2021-09-22 16:06
閱讀 2761·2021-09-10 10:50
閱讀 3624·2021-09-07 10:19
閱讀 1868·2019-08-30 15:53
閱讀 1243·2019-08-29 15:13
閱讀 2888·2019-08-29 13:20