之前的文章我們曾總結到了Redis數據結構——鏈表和Redis數據結構——壓縮列表這兩種數據結構,他們是Redis List(列表)對象的底層實現方式。但是考慮到鏈表的附加空間相對太高,prev 和 next 指針就要占去 16 個字節 (64bit 系統的指針是 8 個字節),另外每個節點的內存都是單獨分配,會加劇內存的碎片化,影響內存管理效率。因此Redis3.2版本開始對列表數據結構進行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.
一、基本結構
quicklist 實際上是 zipList 和 linkedList 的混合體,它將 linkedList 按段切分,每一段使用 zipList 來緊湊存儲,多個 zipList 之間使用雙向指針串接起來。
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;
- prev: 指向鏈表前一個節點的指針。
- next: 指向鏈表后一個節點的指針。
- zl: 數據指針。如果當前節點的數據沒有壓縮,那么它指向一個ziplist結構;否則,它指向一個quicklistLZF結構。
- sz: 表示zl指向的ziplist的總大小(包括
zlbytes
,zltail
,zllen
,zlend
和各個數據項)。需要注意的是:如果ziplist被壓縮了,那么這個sz的值仍然是壓縮前的ziplist大小。 - count: 表示ziplist里面包含的數據項個數。這個字段只有16bit。稍后我們會一起計算一下這16bit是否夠用。
- encoding: 表示ziplist是否壓縮了(以及用了哪個壓縮算法)。目前只有兩種取值:2表示被壓縮了(而且用的是LZF壓縮算法),1表示沒有壓縮。
- container: 是一個預留字段。本來設計是用來表明一個quicklist節點下面是直接存數據,還是使用ziplist存數據,或者用其它的結構來存數據(用作一個數據容器,所以叫container)。但是,在目前的實現中,這個值是一個固定的值2,表示使用ziplist作為數據容器。
- recompress: 當我們使用類似lindex這樣的命令查看了某一項本來壓縮的數據時,需要把數據暫時解壓,這時就設置recompress=1做一個標記,等有機會再把數據重新壓縮。
- attempted_compress: 這個值只對Redis的自動化測試程序有用。我們不用管它。
- extra: 其它擴展字段。目前Redis的實現里也沒用上。
typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF;
quicklistLZF結構表示一個被壓縮過的ziplist。其中:
- sz: 表示壓縮后的ziplist大小。
- compressed: 是個柔性數組(flexible array member),存放壓縮后的ziplist字節數組。
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned long len; /* number of quicklistNodes */
int fill : QL_FILL_BITS; /* fill factor for individual nodes */
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
- head: 指向頭節點(左側第一個節點)的指針。
- tail: 指向尾節點(右側第一個節點)的指針。
- count: 所有ziplist數據項的個數總和。
- len: quicklist節點的個數。
- fill: 16bit,ziplist大小設置,存放
list-max-ziplist-size
參數的值。 - compress: 16bit,節點壓縮深度設置,存放
list-compress-depth
參數的值。
二、常用操作
2.1 插入
quicklist可以選擇在頭部或者尾部進行插入(quicklistPushHead
和quicklistPushTail
),而不管是在頭部還是尾部插入數據,都包含兩種情況:
- 如果頭節點(或尾節點)上ziplist大小沒有超過限制(即
_quicklistNodeAllowInsert
返回1),那么新數據被直接插入到ziplist中(調用ziplistPush
)。 - 如果頭節點(或尾節點)上ziplist太大了,那么新創建一個quicklistNode節點(對應地也會新創建一個ziplist),然后把這個新創建的節點插入到quicklist雙向鏈表中。
也可以從任意指定的位置插入。quicklistInsertAfter
和quicklistInsertBefore
就是分別在指定位置后面和前面插入數據項。這種在任意指定位置插入數據的操作,要比在頭部和尾部的進行插入要復雜一些。
- 當插入位置所在的ziplist大小沒有超過限制時,直接插入到ziplist中就好了;
- 當插入位置所在的ziplist大小超過了限制,但插入的位置位於ziplist兩端,並且相鄰的quicklist鏈表節點的ziplist大小沒有超過限制,那么就轉而插入到相鄰的那個quicklist鏈表節點的ziplist中;
- 當插入位置所在的ziplist大小超過了限制,但插入的位置位於ziplist兩端,並且相鄰的quicklist鏈表節點的ziplist大小也超過限制,這時需要新創建一個quicklist鏈表節點插入。
- 對於插入位置所在的ziplist大小超過了限制的其它情況(主要對應於在ziplist中間插入數據的情況),則需要把當前ziplist分裂為兩個節點,然后再其中一個節點上插入數據。
2.2 查找
list的查找操作主要是對index的我們的quicklist的節點是由一個一個的ziplist構成的每個ziplist都有大小。所以我們就只需要先根據我們每個node的個數,從而找到對應的ziplist,調用ziplist的index就能成功找到。
2.3 刪除
區間元素刪除的函數是 quicklistDelRange
quicklist 在區間刪除時,會先找到 start 所在的 quicklistNode,計算刪除的元素是否小於要刪除的 count,如果不滿足刪除的個數,則會移動至下一個 quicklistNode 繼續刪除,依次循環直到刪除完成為止。
quicklistDelRange 函數的返回值為 int 類型,當返回 1 時表示成功的刪除了指定區間的元素,返回 0 時表示沒有刪除任何元素。
2.4 其它
除了上面介紹的基本操作之外還有一些其它操作,大家可以嘗試着根據鏈表和壓縮列表的數據結構來分析一些quicklist這些操作的時間復雜度。
操作 | 時間復雜度 |
---|---|
quicklistCreate:創建 quicklist | ? |
quicklistInsertAfter:在某個元素的后面添加數據 | ? |
quicklistInsertBefore:在某個元素的前面添加數據 | ? |
quicklistReplaceAtIndex:替換某個元素 | ? |
quicklistDelEntry:刪除單個元素 | ? |
quicklistDelRange:刪除區間元素 | ? |
quicklistPushHead:頭部插入元素 | ? |
quicklistPushTail:尾部插入元素 | ? |
小結
Redis quicklist是Redis 3.2版本以后針對鏈表和壓縮列表進行改造的一種數據結構,是 zipList 和 linkedList 的混合體,相對於鏈表它壓縮了內存。進一步的提高了效率。
如果你有什么疑問,歡迎在評論區給我留言和分享,我會第一時間反饋!我們一起共同學習與進步!
系列文章:
-----END-----
關注下方公眾號,回復“Redis”,可得Redis相關學習資料