基於redis5.0的版本。
redis鏈表(List)字符編碼有:ziplist和quicklist,老版本也有linkedlis。html
3.2
版本以後列表再也不使用linkedlist,這裏列出來是爲了後面的對比。node
struct list { listNode *head; //表頭節點 listNode *tail; //表尾節點 unsigned long len; //鏈表鎖包含的節點數量 void *(*dup)(void *ptr); //節點值複製函數 void *(*free)(void *ptr); //節點值釋放行數 void *(*match)(void *ptr, void *key); //節點值對比函數 } struct listNode { listNode *prev; listNode *next; void *value; }
StringObject就是type爲string的RedisObject對象,在本文中都簡稱爲StringObject。
// ziplist.c unsigned char *ziplistNew(void) { unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE; 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; }
字段 | 類型 | 長度 | 說明 |
---|---|---|---|
zlbytes | uint32_t | 4字節 | 記錄整個壓縮列表佔用的內存字節數,包括 4 字節的 zlbytes 自己。在對壓縮列表進行內存重分配時,或者計算zlend的位置時使用。 |
zltail | uint32_t | 4字節 | 記錄壓縮列表表尾節距離起始地址的字節偏移量,經過這個偏移量,能夠快速肯定最後一個節點的地址。 |
zllen | uint16_t | 2字節 | 記錄壓縮列表中節點的數量。當節點數量超過或者等於 UINT16_MAX(65535) 時,須要遍歷整個列表才能知道節點的數量。 |
entry | zlentry | 根據節點內容 | 壓縮列表包含的各個節點。 |
zlend | uint8_t | 1字節 | 固定值爲0xFF(255),標識壓縮列表的尾節點。其餘普通的節點不會以 255 開頭。所以能夠經過檢查節點的地一個字節是否等於 255 從而知道是否已經到達列表的末尾。 |
下圖實例:
說明:git
entry:
github
prevrawlen記錄前一個節點的長度,屬性的長度是1字節或者5字節。redis
由於prevrawlen記錄了前一個節點的長度,因此程序能夠經過指針運算,根據當前節點的起始地址來計算出前一個節點的起始地址。壓縮列表的從表尾到表頭遍歷操做就是使用這一原理。算法
encoding記錄的是data數據的類型和長度(github的ziplist.c上有詳細說明)。數組
4.當最高位爲11時,encoding長度爲1字節,data保存當是整數值:數據結構
例如:函數
當ziplist插入新節點,或者節點內容變長時,須要追加申請須要的內存空間(ziplist.c文件下的ziplistResize函數,最終是調用object.c下的zrealloc函數),若是沒法追加申請到足夠的內存,則會從新申請一個完整的內存,並將當前ziplist數據複製到新內存空間。
prevlen屬性記錄了前一個節點的長度:假設entry2的前一個節點entry1節點長度小於254字節,則entry2的prevlen只須要1個字節來保存這個長度;若是entry1內容變動(或者在entry1和entry2之間插入新節點;或者刪除entry1使得entry2的前置節點變成entry0),超出來254字節,則entry2的prevlen當前1個字節沒法保存,須要擴展成5個字節,redis須要從新申請內存空間;若是恰好entry2本來的長度介於250~253字節之間,擴展以後,entry2的長度超出來254字節,會致使entry3也出現變動的狀況。最壞狀況下,若是每一個節點都是相似於entry1和entry2的狀況,redis須要不斷地對壓縮列表執行空間從新分配操做(ziplist.c下的__ziplistCascadeUpdate函數,while循環節點,每一個節點都會從新一次申請內存空間)。
儘管連鎖更新的複雜度高,會形成性能問題,可是它出現對概率很低。post
quicklist是redis list的內部實現,是一個ziplist的雙向鏈表:quecklist的每一個節點都是一個ziplist,結合了linkedlist和ziplist的優勢。
// qicklist.h /* 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: 10 bits, free for future use; pads out the remainder of 32 bits */ typedef struct quicklistNode { struct quicklistNode *prev; // 指向上一個ziplist節點 struct quicklistNode *next; // 指向下一個ziplist節點 unsigned char *zl; // 數據指針,若是沒有被壓縮,就指向ziplist結構,反之指向quicklistLZF結構 unsigned int sz; // 表示指向ziplist結構的總長度(內存佔用長度) unsigned int count : 16; // 表示ziplist中的數據項個數 unsigned int encoding : 2; // 編碼方式,1--ziplist,2--quicklistLZF unsigned int container : 2; // 預留字段,存放數據的方式,1--NONE,2--ziplist。原本設計是用來代表一個quicklist節點下面是直接存數據,仍是使用ziplist存數據,或者用其它的結構來存數據(用做一個數據容器,因此叫container)。在目前的實現中,這個值是一個固定的值2,表示使用ziplist做爲數據容器。 unsigned int recompress : 1; // 解壓標記,當查看一個被壓縮的數據時,須要暫時解壓,標記此參數爲1,以後再從新進行壓縮 unsigned int attempted_compress : 1; // 測試相關 unsigned int extra : 10; // 擴展字段,暫時沒用 } 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 { // 表示一個被壓縮過的ziplist unsigned int sz; // LZF壓縮後佔用的字節數 char compressed[]; // 柔性數組,存放壓縮後的ziplist字節數組 } quicklistLZF; /* quicklist is a 40 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; // 指向quicklist的頭部節點 quicklistNode *tail; // 指向quicklist的尾部節點 unsigned long count; // 列表中全部數據項的個數總和 unsigned int len; // 全部ziplist的個數總和 int fill : 16; // ziplist大小限定,由list-max-ziplist-size給定 unsigned int compress : 16; // 節點壓縮深度設置,由list-compress-depth給定 } quicklist;
到底一個quicklist節點包含多長的ziplist合適呢?好比,一樣是存儲12個數據項,既能夠是一個quicklist包含3個節點,而每一個節點的ziplist又包含4個數據項,也能夠是一個quicklist包含6個節點,而每一個節點的ziplist又包含2個數據項。
這又是一個須要找平衡點的難題。咱們只從存儲效率上分析一下:
可見,一個quicklist節點上的ziplist要保持一個合理的長度。那到底多長合理呢?這可能取決於具體應用場景。實際上,Redis提供了一個配置參數list-max-ziplist-size,就是爲了讓使用者能夠來根據本身的狀況進行調整。
咱們來詳細解釋一下這個參數的含義。它能夠取正值,也能夠取負值。
當取正值的時候,表示按照數據項個數來限定每一個quicklist節點上的ziplist長度。好比,當這個參數配置成5的時候,表示每一個quicklist節點的ziplist最多包含5個數據項。
當取負值的時候,表示按照佔用字節數來限定每一個quicklist節點上的ziplist長度。這時,它只能取-1到-5這五個值,每一個值含義以下:
另外,list的設計目標是可以用來存儲很長的數據列表的。好比,Redis官網給出的這個教程:Writing a simple Twitter clone with PHP and Redis,就是使用list來存儲相似Twitter的timeline數據。
當列表很長的時候,最容易被訪問的極可能是兩端的數據,中間的數據被訪問的頻率比較低(訪問起來性能也很低)。若是應用場景符合這個特色,那麼list還提供了一個選項,可以把中間的數據節點進行壓縮,從而進一步節省內存空間。Redis的配置參數list-compress-depth
就是用來完成這個設置的。
這個參數表示一個quicklist兩端不被壓縮的節點個數。注:這裏的節點個數是指quicklist雙向鏈表的節點個數,而不是指ziplist裏面的數據項個數。實際上,一個quicklist節點上的ziplist,若是被壓縮,就是總體被壓縮的。
參數list-compress-depth
的取值含義以下:
因爲0是個特殊值,很容易看出quicklist的頭節點和尾節點老是不被壓縮的,以便於在表的兩端進行快速存取。
Redis對於quicklist內部節點的壓縮算法,採用的LZF——一種無損壓縮算法。
// server.h /* List defaults */ #define OBJ_LIST_MAX_ZIPLIST_SIZE -2 #define OBJ_LIST_COMPRESS_DEPTH 0
以上內容參考自:
《redis設計與實現》
《 Redis源碼剖析系列》
《 Redis內部數據結構詳解系列》
《 多是目前最詳細的Redis內存模型及應用解讀》