做者:李樂 順風車運營研發團隊
壓縮列表是列表鍵和哈希鍵的底層實現之一。redis
當一個列表鍵只包含少許列表項,而且每一個列表項要麼是小整數,要麼是長度較短的字符串時,Redis就會使用壓縮列表來做爲列表鍵的底層實現;數組
當一個哈希鍵只包含少許鍵值對,而且每一個鍵值對的鍵和值要麼就是小整數值要麼就是長度較短的字符串時,Redis就會使用壓縮列表來做爲哈希鍵的底層實現;架構
壓縮列表是爲了節約內存而開發的,其就是一個字節數組(char *);ui
而一個壓縮列表能夠包含任意多個節點(entry),每一個節點能夠保存一個字節數組或一個整數;編碼
zlbytes:4字節,壓縮列表的長度;所以壓縮列表最大(2^32)-1字節;spa
zltail:4字節,尾節點偏移量;指針
zllen:2字節,壓縮列表節點數目,其最大隻能表示65535個節點;當壓縮列表節點數目超過65535後,此字段無就沒有任何意義了;code
entryX:節點ip
zlend:壓縮列表結尾標誌,固定爲0xFF;內存
假設char * zl指向壓縮列表首地址;
注意:zl指針的類型爲char*,所以經過zl獲取相應字段時,首先須要強制類型轉換;
zl指向zlbytes字段;((uint32_t) zl)取出zlbytes字段內容;
zl+4指向zltail字段;((uint32_t) (zl+4))取出zltail字段內容;
((uint32_t) (zl+4)) + zl (就是zl+zltail)指向最後一個節點首地址
zl+8指向zllen字段;((uint16_t) (zl+8))取出zllen字段內容;
zl + ((uint32_t) zl) (就是zl+zlbytes)指向zlend字段
瞭解了壓縮列表結構,咱們能夠很容易得到壓縮列表空間大小,壓縮列表擁有節點數目,壓縮列表開始和結束位置指針;
那麼如何遍歷壓縮列表的全部節點呢?
對於每個entry節點,存儲的多是字節數組或整數值;
假設咱們知道節點首地址指針,咱們如何知道存儲的是什麼類型?對於字節數組,咱們又如何知道字節數組的長度?
redis是對每個entry是這樣編碼的:
2.1 首先回答上面一個問題:壓縮列表如何遍歷全部節點?
答案就在previous_entry_length字段,其表示前一個節點的長度(單位字節);
假如我知道當前節點的首地址爲p,那麼(p-previous_entry_length)就是前一個節點的首地址;經過這種方式實現了從尾到頭的遍歷;
previous_entry_length字段爲1個或者5個字段(爲了節約內存);
當前一個節點的長度小於254字節時,previous_entry_length字段用一個字段表示;
當前一個節點的長度大於等於254時,previous_entry_length字段用5個字節來表示;而這時候previous_entry_length的第一個字節是固定的標誌0xFE,後面4個字節才真正表示前一個節點的長度;
假設當前節點首地址爲p;p[0]爲第一個字節內容;
當p[0]<0xFE時,說明previous_entry_length字段只佔一個字節,p[0]就是前一個節點的長度;
當p[0]=0xFE時,說明previous_entry_length字段佔5個字節,p[1]~p[4]表示前一個節點的長度;而p+5則只encoding字段首地址;
2.2 下面回答第二個問題:
如何區分當前節點存儲數據是什麼類型,字節數組仍是整數?字節數組長度?
最簡單的方法:使用1個比特表示數據是字節數組仍是整數,假如是字節數組,再用7+4*8表示字節數組的長度;
可是,redis爲了節約內存並無這麼作;(減小encoding字段長度)
字節數組分爲三種,最大長度63字節,最大長度(2^14)-1,最大長度(2^32)-1;
整數分爲6種:8比特整數,24比特整數,int16,int32,int64,0~12當即數;
而具體的數據內容存儲在content字段;
咱們發現encoding第一個字節的前2比特能夠區分是字節數組(以及字節數組類型)仍是整數;
是整數時,第三、4比特能夠區分整數的類型;當content的前4個比特都是1時,後4個比特才能區分整數類型;
假設encoding字段首地址爲p;p[0]爲第一個字節內容;
p[0] & 0xc0 能夠得到前兩個比特bit[1:2],當其不等於11B時,說明content是字節數組;再根據其是00B、01B仍是10B能夠知道字節數組類型,從而取出字節數組實際長度;
整數類型的判斷同理可得;
2.1 大端小端
redis在存取壓縮列表字段(如zlbytes、zltail時,會進行大小端轉換;若是是小端不作處理,若是是大端,會轉換爲小端字節順序);
大小端轉換其實就是交換字節順序;
void memrev32(void *p) { unsigned char *x = p, t; t = x[0]; x[0] = x[3]; x[3] = t; t = x[1]; x[1] = x[2]; x[2] = t; }
問題:爲何在存取壓縮列表字段時須要作大小端轉換?
解答:redis集羣,各機器的CPU架構可能不相同;有些機器是大端,有些機器是小端;假如不進行大小端轉換,當壓縮列表數據在集羣中機器間傳遞時,不一樣機器解析狀況會不相同。
如圖,位置p處的節點爲X;其previous_entry_length字段爲1個字節,0x80,代表前一個節點長度爲128;假設位置p以後的全部節點的長度爲253字節;
如今往位置p新添加一個節點,其長度爲1024字節;顯然節點X的previous_entry_length須要改變爲5個字節,那麼此時節點X的長度爲257字節;
節點X的長度從253改變爲257字節;那麼節點X的後驅節點的previous_entry_length也須要從一個字節改變爲5個字節;
以此類推;由於在位置P新添加了一個節點,可能致使P後面得全部節點都須要依次更新previous_entry_length字段長度;
這就是連鎖更新;他會致使N次內存分配,效率很低;
可是須要指出的是,這種狀況出現的機率是很低的;並且通常狀況下壓縮列表存儲的節點數目比較少;所以redis並無對這種狀況作特殊處理;