天天看點

Redis知識梳理(28) [ 壓縮清單 ]增加元素級聯更新IntSet 小整數集合

Redis 為了節約記憶體空間使用,zset 和 hash 容器對象在元素個數較少的時候, 采用壓縮清單 (ziplist) 進行存儲。壓縮清單是一塊連續的記憶體空間,元素之間緊 挨着存儲,沒有任何備援空隙。

> zadd programmings 1.0 go 2.0 python 3.0 java

(integer) 3

> debug object programmings

Value at:0x7fec2de00020 refcount:1 encoding:ziplist serializedlength:36 lru:6022374 lru_seconds_idle:6 > hmset books go fast python slow java fast

OK

> debug object books

Value at:0x7fec2de000c0 refcount:1 encoding:ziplist serializedlength:48 lru:6022478 lru_seconds_idle:1

這裡,注意觀察 debug object 輸出的 encoding 字段都是 ziplist,這就表示内 部采用壓縮清單結構進行存儲。

struct ziplist {

int32 zlbytes; // 整個壓縮清單占用位元組數

int32 zltail_offset; // 最後一個元素距離壓縮清單起始位置的偏移量,用于快速定位到最後一個節點

​ int16 zllength; // 元素個數

T[] entries; // 元素内容清單,挨個挨個緊湊存儲

int8 zlend; // 标志壓縮清單的結束,值恒為 0xFF

}

Redis知識梳理(28) [ 壓縮清單 ]增加元素級聯更新IntSet 小整數集合

壓縮清單為了支援雙向周遊,是以才會有 ztail_offset 這個字段,用來快速定 位到最後一個元素,然後倒着周遊

entry 塊随着容納的元素類型不同,也會有不一樣的結構.

struct entry {

int prevlen; // 前一個 entry 的位元組長度

​ int encoding; // 元素類型編碼

optional byte[] content; // 元素内容

}

它的 prevlen 字段表示前一個 entry 的位元組長度,當壓縮清單倒着周遊時,需 要通過這個字段來快速定位到下一個元素的位置。它是一個變長的整數,當字元 串長度小于 254(0xFE) 時,使用一個位元組表示;如果達到或超出 254(0xFE) 那 就使用 5 個位元組來表示。第一個位元組是 0xFE(254),剩餘四個位元組表示字元串長 度。你可能會覺得用 5 個位元組來表示字元串長度,是不是太浪費了。我們可以算 一下,當字元串長度比較長的時候,其實 5 個位元組也隻占用了不到 (5/(254+5)) <2% 的空間。

Redis知識梳理(28) [ 壓縮清單 ]增加元素級聯更新IntSet 小整數集合
Redis知識梳理(28) [ 壓縮清單 ]增加元素級聯更新IntSet 小整數集合

encoding 字段存儲了元素内容的編碼類型資訊,ziplist 通過這個字段來決定後 面的 content 内容的形式。

Redis 為了節約存儲空間,對 encoding 字段進行了相當複雜的設計。Redis 通 過這個字段的字首位來識别具體存儲的資料形式。下面我們來看看 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 ,因為 0000、1110、1111 都被占用了。讀取到的 value 需要将 xxxx

減 1,也就是整數 0~12 就是最終的 value。

注意到 content 字段在結構體中定義為 optional 類型,表示這個字段是可選的,

對于很小的整數而言,它的内容已經内聯到 encoding 字段的尾部了。

增加元素

因為 ziplist 都是緊湊存儲,沒有備援空間 (對比一下 Redis 的字元串結構)。意 味着插入一個新的元素就需要調用 realloc 擴充記憶體。取決于記憶體配置設定器算法和 目前的 ziplist 記憶體大小,realloc 可能會重新配置設定新的記憶體空間,并将之前的内 容一次性拷貝到新的位址,也可能在原有的位址上進行擴充,這時就不需要進行 舊内容的記憶體拷貝。

如果 ziplist 占據記憶體太大,重新配置設定記憶體和拷貝記憶體就會有很大的消耗。是以 ziplist 不适合存儲大型字元串,存儲的元素也不宜過多。

級聯更新

unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {

size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;

size_t offset, noffset, extra;

unsigned char *np;

zlentry cur, next;

while (p[0] != ZIP_END) {

zipEntry(p, &cur);

rawlen = cur.headersize + cur.len;

rawlensize = zipStorePrevEntryLength(NULL,rawlen);

if (p[rawlen] == ZIP_END) break;

zipEntry(p+rawlen, &next);

// prevlen 的長度沒有變,中斷級聯更新

if (next.prevrawlen == rawlen) break;

​ if (next.prevrawlensize < rawlensize) {

// 級聯擴充

offset = p-zl;

extra = rawlensize-next.prevrawlensize;

​ // 擴大記憶體

zl = ziplistResize(zl,curlen+extra);

p = zl+offset;

np = p+rawlen;

noffset = np-zl;

// 更新 zltail_offset 指針

if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {

ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);

}

// 移動記憶體

memmove(np+rawlensize, np+next.prevrawlensize, curlen-noffset-next.prevrawlensize-1);

​ zipStorePrevEntryLength(np,rawlen);

p += rawlen;

curlen += extra;

​ } else {

​ if (next.prevrawlensize > rawlensize) {

break;

}

}

return zl;

}

前面提到每個 entry 都會有一個 prevlen 字段存儲前一個 entry 的長度。如果 内容小于 254 位元組, prevlen 用 1 位元組存儲,否則就是 5 位元組。這意味着如果 某個 entry 經過了修改操作從 253 位元組變成了 254 位元組,那麼它的下一個 entry 的 prevlen 字段就要更新,從 1 個位元組擴充到 5 個位元組;如果這個 entry 的長度本來也是 253 位元組,那麼後面 entry 的 prevlen 字段還得繼續更 新。

如果 ziplist 裡面每個 entry 恰好都存儲了 253 位元組的内容,那麼第一個 entry 内容的修改就會導緻後續所有 entry 的級聯更新,這就是一個比較耗費計算資源 的操作。

删除中間的某個節點也可能會導緻級聯更新,讀者可以思考一下為什麼?

IntSet 小整數集合

當 set 集合容納的元素都是整數并且元素個數較小時,Redis 會使用 intset 來 存儲結合元素。 intset 是緊湊的數組結構,同時支援 16 位、32 位和 64 位整 數。

struct intset {

int32 encoding; // 決定整數位寬是 16 位、32 位還是 64 位

int32 length; // 元素個數

int contents; // 整數數組,可以是 16 位、32 位和 64 位

}

Redis知識梳理(28) [ 壓縮清單 ]增加元素級聯更新IntSet 小整數集合

> sadd codehole 1 2 3

(integer) 3

> debug object codehole

Value at:0x7fec2dc2bde0 refcount:1 encoding:intset serializedlength:15 lru:6065795 lru_seconds_idle:4

> sadd codehole go java python

(integer) 3

> debug object codehole

Value at:0x7fec2dc2bde0 refcount:1 encoding:hashtable serializedlength:22 lru:6065810 lru_seconds_idle:5

注意觀察 debug object 的輸出字段 encoding 的值,可以發現當 set 裡面放進 去了非整數值時,

存儲形式立即從 intset 轉變成了 hash 結構。

繼續閱讀