前言
在版本3.2之前,Redis 列表list使用兩種數據結構作為底層實現:
- 壓縮列表ziplist
- 雙向鏈表linkedlist
- 默認為linkedlist
雙向鏈表linkedlist
Redis實現的是標准的雙向鏈表。
鏈表節點定義:
鏈表定義:
總結鏈表實現:
1.每個節點有前后節點指針,且第一個節點的指針為NULL,最后一個節點的指針為NULL(無環)。
2.對雙鏈表進行封裝,鏈表第一個節點和最后一個節點指針,以及鏈表長度。
優點:
在鏈表兩端進行push和pop操作都是O(1)。
獲取鏈表的長度操作O(1)。
多態,可以存儲c語言支持的任何數據類型,通過void *萬能指針。
缺點:
內存開銷比較大。每個節點上除了要保存數據之外,還要額外保存兩個指針
各個節點是單獨的內存塊,地址不連續,節點多了容易產生內存碎片。
壓縮列表轉化成雙向鏈表條件
創建新列表時 redis 默認使用 redis_encoding_ziplist 編碼, 當以下任意一個條件被滿足時, 列表會被轉換成 redis_encoding_linkedlist 編碼:
- 試圖往列表新添加一個字符串值,且這個字符串的長度超過 server.list_max_ziplist_value (默認值為 64 )。
- ziplist 包含的節點超過 server.list_max_ziplist_entries (默認值為 512 )。
注意:這兩個條件是可以修改的,在 redis.conf 中
list-max-ziplist-value 64 list-max-ziplist-entries 512
ziplist
壓縮列表 ziplist 是為 Redis 節約內存而開發的。
ziplist 是由一系列特殊編碼的內存塊構成的列表(像內存連續的數組,但每個元素長度不同), 一個 ziplist 可以包含多個節點(entry)。
ziplist 將表中每一項存放在前后連續的地址空間內,每一項因占用的空間不同,而采用變長編碼。
當元素個數較少時,Redis 用 ziplist 來存儲數據,當元素個數超過某個值時,鏈表鍵中會把 ziplist 轉化為 linkedlist,字典鍵中會把 ziplist 轉化為 hashtable。
由於內存是連續分配的,所以遍歷速度很快。
在3.2之后,ziplist被quicklist替代。但是仍然是zset底層實現之一。
ziplist內存布局
ziplist使用連續的內存塊,每一個節點(entry)都是連續存儲的;ziplist 存儲分布如下:
area |<---- ziplist header ---->|<----------- entries ------------->|<-end->| size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte +---------+--------+-------+--------+--------+--------+--------+-------+ component | zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend | +---------+--------+-------+--------+--------+--------+--------+-------+ ^ ^ ^ address | | | ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END | ZIPLIST_ENTRY_TAIL
常態的壓縮列表內存布局如上圖所示,整個內存塊區域內分為五個部分,下面分別介紹着五個部分:
- zlbytes:存儲一個無符號整數,固定四個字節長度,用於存儲壓縮列表所占用的字節,當重新分配內存的時候使用,不需要遍歷整個列表來計算內存大小。
- zltail:存儲一個無符號整數,固定四個字節長度,代表指向列表尾部的偏移量,偏移量是指壓縮列表的起始位置到指定列表節點的起始位置的距離。
- zllen:壓縮列表包含的節點個數,固定兩個字節長度,源碼中指出當節點個數大於2^16-2個數的時候,該值將無效,此時需要遍歷列表來計算列表節點的個數。
- entries:列表節點區域,長度不定,由列表節點緊挨着組成。每個節點可以保存一個字節數組或者是一個整數值。
- zlend:一字節長度固定值為255,用於表示列表結束。
上面介紹了壓縮列表的總體內存布局,對於初entries區域以外的四個區域的長度都是固定的,下面再看看此區域中每個節點的布局情況。
每個列表節點由三部分組成:
- previous length:記錄前一個節點所占有的內存字節數,通過該值,我們可以從當前節點計算前一個節點的地址,可以用來實現從表尾向表頭節點遍歷;
- len/encoding:記錄了當前節點content占有的內存字節數及其存儲類型,用來解析content用;
- content:保存了當前節點的值。
最關鍵的是prevrawlen和len/encoding,content只是實際存儲數值的比特位。
為了節省內存,根據上一個節點的長度prevlength 可以將entry節點分為兩類:

- entry的前8位小於254,則這8位就表示上一個節點的長度
- entry的前8位等於254,則意味着上一個節點的長度無法用8位表示,后面32位才是真實的prevlength。用254 不用255(11111111)作為分界是因為255是zlend的值,它用於判斷ziplist是否到達尾部。
根據當前節點存儲的數據類型及長度,可以將ziplist節點分為9類:
其中整數節點分為6類:

#define ZIP_INT_16B (0xc0 | 0<<4)//整數data,占16位(2字節) #define ZIP_INT_32B (0xc0 | 1<<4)//整數data,占32位(4字節) #define ZIP_INT_64B (0xc0 | 2<<4)//整數data,占64位(8字節) #define ZIP_INT_24B (0xc0 | 3<<4)//整數data,占24位(3字節) #define ZIP_INT_8B 0xfe //整數data,占8位(1字節) /* 4 bit integer immediate encoding */ //整數值1~13的節點沒有data,encoding的低四位用來表示data #define ZIP_INT_IMM_MASK 0x0f #define ZIP_INT_IMM_MIN 0xf1 /* 11110001 */ #define ZIP_INT_IMM_MAX 0xfd /* 11111101 */
值得注意的是 最后一種encoding是存儲整數0~12的節點的encoding,它沒有額外的data部分,encoding的高4位表示這個類型,低4位就是它的data。這種類型的節點的encoding大小介於ZIP_INT_24B與ZIP_INT_8B之間(1~13),但是為了表示整數0,取出低四位xxxx之后會將其-1作為實際的data值(0~12)。在函數zipLoadInteger中,我們可以看到這種類型節點的取值方法:
- 當data小於63字節時(2^6),節點存為上圖的第一種類型,高2位為00,低6位表示data的長度。
- 當data小於16383字節時(2^14),節點存為上圖的第二種類型,高2位為01,后續14位表示data的長度。
- 當data小於4294967296字節時(2^32),節點存為上圖的第二種類型,高2位為10,下一字節起連續32位表示data的長度。
字符串節點分為3類:
- 當data小於63字節時(2^6),節點存為上圖的第一種類型,高2位為00,低6位表示data的長度。
- 當data小於16383字節時(2^14),節點存為上圖的第二種類型,高2位為01,后續14位表示data的長度。
- 當data小於4294967296字節時(2^32),節點存為上圖的第二種類型,高2位為10,下一字節起連續32位表示data的長度。
上圖可以看出:不同於整數節點encoding永遠是8位,字符串節點的encoding可以有8位、16位、40位三種長度
相同encoding類型的整數節點 data長度是固定的,但是相同encoding類型的字符串節點,data長度取決於encoding后半部分的值。
#define ZIP_STR_06B (0 << 6)//字符串data,最多有2^6字節(encoding后半部分的length有6位,length決定data有多少字節) #define ZIP_STR_14B (1 << 6)//字符串data,最多有2^14字節 #define ZIP_STR_32B (2 << 6)//字符串data,最多有2^32字節
從尾部向頭部遍歷(利用 ztail 和privious_entry_length),用指向當前節點的指針 e , 減去前一個 entry的長度, 得出的結果就是指向前一個節點的地址 p 。
已知節點的位置,求data的值
entry布局 可以看出,若要算出data的偏移量,得先計算出prevlength所占內存大小(1字節和5字節):
//根據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所占的字節,返回給lensize;data所占的字節返回給len
//根據ptr指向的entry求出該entry的len(encoding里存的 data所占字節)和lensize(encoding所占的字節) #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個字節:00000000、01000000、10000000(字符串類型)和11??????(整數類型) //如果是整數類型,encoding直接照抄ptr的;如果是字符串類型,encoding被截斷成一個字節並清零后6位 #define ZIP_ENTRY_ENCODING(ptr, encoding) do { \ (encoding) = (ptr[0]); \ if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; \ } while(0) //根據encoding返回數據(整數)所占字節數 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
連鎖更新
每個節點的previous_entry_length屬性都記錄了前一個節點的長度
如果前一個節點的長度小於254,那么previous_entry_length屬性需要用1字節長的空間來保存這個長度值
如果前一個節點的長度大於等於254,那么previous_entry_length屬性需要5字節長的空間來保存這個長度值
考慮這樣一種情況:在一個壓縮列表中,有多個連續的、長度介於250字節到253字節之間的節點e1至eN
|zlbytes|zltail|zllen|e1|e2|e3|...|eN|zlend|
因為e1至eN的所有節點的長度都小於254字節,所以記錄這些節點的長度只需要1字節長的previous_entry_length屬性,換句話說,e1至eN的所有節點的previous_entry_length屬性都是1字節長的。
如果我們將一個長度大於等於254字節的新節點new設置到壓縮列表的表頭節點,那么new將成為e1的潛質節點。
此時e1到eN的每個節點的previous_entry_length屬性都要擴展為5字節以符合壓縮列表對節點的要求,程序需要不斷的對壓縮列表進行空間重分配操作。
Redis將這種在特殊情況下產生的多次空間擴展操作稱之為“連鎖更新”。
除了添加新節點可能會引發連鎖更新之外,刪除節點也可能會連鎖更新。
因為連鎖更新在最壞情況下需要對壓縮列表執行N次空間重分配操作,而每次空間重分配的最壞復雜度為O(N),所以連鎖更新的最壞復雜度為O(N2)。
注意的是,盡管連鎖更新的復雜度較高,但它真正趙成性能問題的幾率是很低的:
首先,壓縮列表里要恰好有多個連續的、長度介於250字節至253字節之間的節點,連鎖更新才有可能被引發,在實際中,這種情況並不多見;
其次,即使出現連鎖更新,但只要更新的節點數量不多,就不會對性能造成任何影響:比如說,對三五個節點進行連鎖更新是絕對不會影響性能的;
Redis中壓縮列表的應用
Redis中,不同的數據類型廣泛地應用了壓縮列表編碼,整理如下表:

ziplist總結
quickList
可以認為quickList,是ziplist和linkedlist二者的結合;quickList將二者的優點結合起來。
官方給出的定義:
A generic doubly linked quicklist implementation
A doubly linked list of ziplists
quickList是一個ziplist組成的雙向鏈表。每個節點使用ziplist來保存數據。
本質上來說,quicklist里面保存着一個一個小的ziplist。結構如下:

/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist. * We use bit fields keep the quicklistNode at 32 bytes. * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k). * encoding: 2 bits, RAW=1, LZF=2. * container: 2 bits, NONE=1, ZIPLIST=2. * recompress: 1 bit, bool, true if node is temporarry decompressed for usage. * attempted_compress: 1 bit, boolean, used for verifying during testing. * extra: 12 bits, free for future use; pads out the remainder of 32 bits */ typedef struct quicklistNode { struct quicklistNode *prev; //上一個node節點 struct quicklistNode *next; //下一個node unsigned char *zl; //保存的數據 壓縮前ziplist 壓縮后壓縮的數據 unsigned int sz; /* ziplist size in bytes */ unsigned int count : 16; /* count of items in ziplist */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */ unsigned int recompress : 1; /* was this node previous compressed? */ unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode; /* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'. * 'sz' is byte length of 'compressed' field. * 'compressed' is LZF data with total (compressed) length 'sz' * NOTE: uncompressed length is stored in quicklistNode->sz. * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */ typedef struct quicklistLZF { unsigned int sz; /* LZF size in bytes*/ char compressed[]; } quicklistLZF; /* quicklist is a 32 byte struct (on 64-bit systems) describing a quicklist. * 'count' is the number of total entries. * 'len' is the number of quicklist nodes. * 'compress' is: -1 if compression disabled, otherwise it's the number * of quicklistNodes to leave uncompressed at ends of quicklist. * 'fill' is the user-requested (or default) fill factor. */ typedef struct quicklist { quicklistNode *head; //頭結點 quicklistNode *tail; //尾節點 unsigned long count; /* total count of all entries in all ziplists */ unsigned int len; /* number of quicklistNodes */ int fill : 16; /* fill factor for individual nodes *///負數代表級別,正數代表個數 unsigned int compress : 16; /* depth of end nodes not to compress;0=off *///壓縮級別 } quicklist;
quickList就是一個標准的雙向鏈表的配置,有head 有tail;
每一個節點是一個quicklistNode,包含prev和next指針。
每一個quicklistNode 包含 一個ziplist,*zp 壓縮鏈表里存儲鍵值。
所以quicklist是對ziplist進行一次封裝,使用小塊的ziplist來既保證了少使用內存,也保證了性能。
refer:
《Redis設計與實現》