Redis 底層使用了 ziplist、skiplist 和 quicklist 三種 list 結構來實現相關對象。顧名思義,ziplist 更節省空間、skiplist 則注重查找效率,quicklist 則對空間和時間進行折中。node
在典型的雙向鏈表中,咱們有稱爲節點的結構,它表示列表中的每一個值。每一個節點都有三個屬性:指向列表中的前一個和下一個節點的指針,以及指向節點中字符串的指針。而每一個值字符串值實際上存儲爲三個部分:一個表示長度的整數、一個表示剩餘空閒字節數的整數以及字符串自己後跟一個空字符。redis
能夠看到,鏈表中的每一項都佔用獨立的一塊內存,各項之間用地址指針(或引用)鏈接起來。這種方式會帶來大量的內存碎片,並且地址指針也會佔用額外的內存。這就是普通鏈表的內存浪費問題。算法
此外,在普通鏈表中執行隨機查找操做時,它的時間複雜度爲 O(n),這對於注重效率的 Redis 而言也是不可接受的。這是普通鏈表的查找效率過低問題。數組
針對上述兩個問題,Redis 設計了ziplist(壓縮列表)、skiplist(跳躍表)和快速鏈表進行相關優化。數據結構
對於 ziplist,它要解決的就是內存浪費的問題。也就是說,它的設計目標就是是爲了節省空間,提升存儲效率。性能
基於此,Redis 對其進行了特殊設計,使其成爲一個通過特殊編碼的雙向鏈表。將表中每一項存放在先後連續的地址空間內,一個 ziplist 總體佔用一大塊內存。它是一個表(list),但其實不是一個鏈表(linked list)。優化
除此以前,ziplist 爲了在細節上節省內存,對於值的存儲採用了變長的編碼方式,大概意思是說,對於大的整數,就多用一些字節來存儲,而對於小的整數,就少用一些字節來存儲。ui
也正是爲了這種高效率的存儲,ziplist 有不少 bit 級別的操做,使得代碼變得較爲晦澀難懂。不過沒關係,咱們本節的目標之一是爲了瞭解 ziplist 對比普通鏈表,作了哪些優化,能夠更好的節省空間。this
接下來咱們來正式認識下壓縮列表的結構。編碼
一個壓縮列表能夠包含任意多個節點(entry),每一個節點能夠保存一個字節數組或者一個整數值。
圖 1-1 展現了壓縮列表的各個組成部分:
相關字段說明以下:
圖 1-2 展現了一個包含五個節點的壓縮列表:
節點的結構源碼以下(ziplist.c):
typedef struct zlentry { unsigned int prevrawlensize, prevrawlen; unsigned int lensize, len; unsigned int headersize; unsigned char encoding; unsigned char *p; } zlentry;
如圖 1-3,展現了壓縮列表節點的結構。
回到咱們最開始對普通鏈表的認識,普通鏈表中,每一個節點包:
以圖 1-4 爲例:
圖 1-4 展現了一個普通鏈表的三個節點,這三個節點中,每一個節點實際存儲內容只有 1 字節,可是它們除了實際存儲內容外,還都要有:
這樣來看,存儲 3 個字節的數據,至少須要 21 字節的開銷。能夠看到,這樣的存儲效率是很低的。
另外一方面,普通鏈表經過先後指針來關聯節點,地址不連續,多節點時容易產生內存碎片,下降了內存的使用率。
最後,普通鏈表對存儲單位的操做粒度是 byte,這種方式在存儲較小整數或字符串時,每一個字節實際上會有很大的空間是浪費的。就像上面三個節點中,用來存儲剩餘空閒字節數的整數,實際存儲空間只須要 1 bit,可是有了 1 byte 來表示剩餘空間大小,這一個 byte 中,剩餘 7 個 bit 就被浪費了。
那麼,Redis 是如何使用 ziplist 來改造普通鏈表的呢?經過如下兩方面:
一方面,ziplist 使用一整塊連續內存,避免產生內存碎片,提升了內存的使用率。
另外一方面,ziplist 將存儲單位的操做粒度從 byte 下降到 bit,有效的解決了存儲較小數據時,單個字節中浪費 bit 的問題。
skiplist 是一種有序數據結構,它經過在每一個節點中維持多個指向其餘節點的指針,來達到快速訪問節點的目的。
skiplist 本質上是一種查找結構,用於解決算法中的查找問題。即根據指定的值,快速找到其所在的位置。
此外,咱們知道,"查找" 問題的解決方法通常分爲兩大類:平衡樹和哈希表。有趣的是,skiplist 這種查找結構,由於其特殊性,並不在上述兩大類中。但在大部分狀況下,它的效率能夠喝平衡樹想媲美,並且跳躍表的實現要更爲簡單,因此有很多程序都使用跳躍表來代替平衡樹。
本節沒有介紹跳躍表的定義及其原理,有興趣的童鞋能夠參考這裏。
認識了跳躍表是什麼,以及作什麼的,接下來,咱們再來看下在 redis 中,是怎麼實現跳躍表的。
在 server.h
中能夠找到跳躍表的源碼,以下:
typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; int level; } zskiplist; typedef struct zskiplistNode { robj *obj; double score; struct zskiplistNode *backward; struct zskiplistLevel { struct zskiplistNode *forward; unsigned int span; } level[]; } zskiplistNode;
Redis 中的 skiplist 和普通的 skiplist 相比,在結構上並無太大不一樣,只是在一些細節上有如下差別:
對於 quicklist,在 quicklist.c
中有如下說明:
A doubly linked list of ziplists
它是一個雙向鏈表,而且是一個由 ziplist 組成的雙向鏈表。
相關源碼結構可在 quicklist.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: 12 bits, free for future use; pads out the remainder of 32 bits */ typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; 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 而言,它的每個節點都是一個 ziplist。quicklist 這樣設計,其實就是咱們篇頭所說的,是一個空間和時間的折中。
ziplist 相比普通鏈表,主要優化了兩個點:下降內存開銷和減小內存碎片。正所謂,事物老是有兩面性。ziplist 經過連續內存解決了普通鏈表的內存碎片問題,但與此同時,也帶來了新的問題:不利於修改操做。
因爲 ziplist 是一整塊連續內存,因此每次數據變更都會引起一次內存的重分配。當在 ziplist 很大的時候,每次重分配都會出現大批量的數據拷貝操做,下降性能。
因而,結合了雙向鏈表和 ziplist 的優勢,就有了 quicklist。
quicklist 的基本思想就是,給每個節點的 ziplist 分配合適的大小,避免出現因數據拷貝,下降性能的問題。這又是一個須要找平衡點的難題。咱們先從存儲效率上分析:
可見,一個 quicklist 節點上的 ziplist 須要保持一個合理的長度。這裏的合理取決於實際應用場景。基於此,Redis 提供了一個配置參數,讓使用者能夠根據狀況,本身調整:
list-max-ziplist-size -2
這個參數能夠取正值,也能夠取負值。
當取正值的時候,表示按照數據項個數來限定每一個 quicklist 節點上 ziplist 的長度。好比配置爲 2 時,就表示 quicklist 的每一個節點上的 ziplist 最多包含 2 個數據項。
當取負值的時候,表示按照佔用字節數來限定每一個 quicklist 節點上 ziplist 的長度。此時,它的取值範圍是 [-1, -5],每一個值對應不一樣含義: