為了加快速度,Redis都做了哪些“變態”設計
- 前言
- 串列物件
- linkedlist
- linkedlist存盤結構
- ziplist
- linkedlist 和 ziplist 的選擇
- quicklist
- quicklist 內部存盤結構
- quicklist 的 compress 屬性
- quicklistNode 的 zl 指標
- quicklist 對比原始兩種編碼的改進
- 串列物件常用操作命令
- 總結
前言
本文GitHub已收錄:https://zhouwenxing.github.io/
串列物件是 Redis 中 5 種基礎資料型別之一,在 Redis 3.2 版本之前,串列物件底層存盤結構有兩種:linkedlist(雙端串列)和 ziplist(壓縮串列),而在 Redis 3.2 版本之后,串列物件底層存盤結構只有一種:quicklist(快速串列),難道通過精心設計的 ziplist 最終被 Redis 拋棄了嗎?
串列物件
同字串物件一樣,串列物件到底使用哪一種資料結構來進行存盤也是通過編碼來進行區分:
| 編碼屬性 | 描述 | object encoding命令回傳值 |
|---|---|---|
| OBJ_ENCODING_LINKEDLIST | 使用 linkedlist 實作串列物件 | linkedlist |
| OBJ_ENCODING_ZIPLIST | 使用 ziplist 實作串列物件 | ziplist |
| OBJ_ENCODING_QUICKLIST | 使用 quicklist 實作串列物件 | quicklist |
linkedlist
linkedlist 是一個雙向串列,每個節點都會存盤指向上一個節點和指向下一個節點的指標,linkedlist 因為每個節點之間的空間是不連續的,所以可能會造成過多的記憶體空間碎片,
linkedlist存盤結構
鏈表中每一個節點都是一個 listNode 物件(原始碼 adlist.h 內),不過需要注意的是,串列中的 value 其實也是一個字串物件,其他幾種資料型別其內部最終也是會嵌套字串物件,字串物件也是唯一一種會被其他物件參考的基本型別:
typedef struct listNode {
struct listNode *prev;//前一個節點
struct listNode *next;//后一個節點
void *value;//值(字串物件)
} listNode;
然后會將其再進行封裝成為一個 list 物件(原始碼 adlist.h 內):
typedef struct list {
listNode *head;//頭節點
listNode *tail;//尾節點
void *(*dup)(void *ptr);//節點值復制函式
void (*free)(void *ptr);//節點值釋放函式
int (*match)(void *ptr, void *key);//節點值對比函式
unsigned long len;//節點數量
} list;
Redis 中對 linkedlist 的訪問是以 NULL 值為終點的,因為 head 節點的 prev 節點為 NULL,tail 節點的 next 節點也為 NULL,所以從頭節點開始遍歷,當發現 tail 為 NULL 時,則可以認為已經到了串列末尾,
當我們設定一個串列物件時,在 Redis 3.2 版本之前我們可以得到如下存盤示意圖:

ziplist
壓縮串列在前面已經介紹過,想要詳細了解的可以點擊這里,
linkedlist 和 ziplist 的選擇
在 Redis3.2 之前,linkedlist 和 ziplist 兩種編碼可以進選擇切換,如果需要串列使用 ziplist 編碼進行存盤,則必須滿足以下兩個條件:
- 串列物件保存的所有字串元素的長度都小于
64位元組, - 串列物件保存的元素數量小于
512個,
一旦不滿足這兩個條件的任意一個,則會使用 linkedlist 編碼進行存盤,
PS:這兩個條件可以通過引數 list-max-ziplist-value 和 list-max-ziplist-entries 進行修改,
這兩種串列能在特定的場景下發揮各自的作用,應該來說已經能滿足大部分需求了,然后 Redis 并不滿足于此,于是一場改革引發了,quicklist 橫空出世,
quicklist
在 Redis 3.2 版本之后,為了進一步提升 Redis 的性能,串列物件統一采用 quicklist 來存盤串列物件,quicklist存盤了一個雙向串列,每個串列的節點是一個 ziplist,所以實際上 quicklist 并不是一個新的資料結構,它就是linkedlist 和 ziplist 的結合,然后被命名為快速串列,
quicklist 內部存盤結構
quicklist 中每一個節點都是一個 quicklistNode 物件,其資料結構定義如下:
typedef struct quicklistNode {
struct quicklistNode *prev;//前一個節點
struct quicklistNode *next;//后一個節點
unsigned char *zl;//當前指向的ziplist或者quicklistLZF
unsigned int sz;//當前ziplist占用位元組
unsigned int count : 16;//ziplist中存盤的元素個數,16位元組(最大65535個)
unsigned int encoding : 2; //是否采用了LZF壓縮演算法壓縮節點 1:RAW 2:LZF
unsigned int container : 2; //存盤結構,NONE=1, ZIPLIST=2
unsigned int recompress : 1; //當前ziplist是否需要再次壓縮(如果前面被解壓過則為true,表示需要再次被壓縮)
unsigned int attempted_compress : 1;//測驗用
unsigned int extra : 10; //后期留用
} quicklistNode;
然后各個 quicklistNode 就構成了一個快速串列 quicklist:
typedef struct quicklist {
quicklistNode *head;//串列頭節點
quicklistNode *tail;//串列尾節點
unsigned long count;//ziplist中一共存盤了多少元素,即:每一個quicklistNode內的count相加
unsigned long len; //雙向鏈表的長度,即quicklistNode的數量
int fill : 16;//填充因子
unsigned int compress : 16;//壓縮深度 0-不壓縮
} quicklist;
根據這兩個結構,我們可以得到 Redis 3.2 版本之后的串列物件的一個存盤結構示意圖:

quicklist 的 compress 屬性
compress 是用來表示壓縮深度,ziplist 除了記憶體空間是連續之外,還可以采用特定的 LZF 壓縮演算法來將節點進行壓縮存盤,從而更進一步的節省空間,壓縮深度可以通過引數 list-compress-depth 控制:
- 0:不壓縮(默認值)
- 1:首尾第1個元素不壓縮
- 2:首位前2個元素不壓縮
- 3:首尾前3個元素不壓縮
- 以此類推
注意:之所以采取這種壓縮兩端節點的方式是因為很多場景都是兩端的元素訪問率最高的,而中間元素訪問率相對較低,所以在實際使用時,我們可以根據自己的實際情況選擇是否進行壓縮,以及具體的壓縮深度,
quicklistNode 的 zl 指標
zl 指標默認指向了 ziplist,上面提到 quicklistNode 中有一個 sz 屬性記錄了當前 ziplist 占用的位元組,不過這僅僅限于當前節點沒有被壓縮(通過LZF 壓縮演算法)的情況,如果當前節點被壓縮了,那么被壓縮節點的 zl 指標會指向另一個物件 quicklistLZF,而不會直接指向 ziplist,quicklistLZF 是一個 4+N 位元組的結構:
typedef struct quicklistLZF {
unsigned int sz;// LZF大小,占用4位元組
char compressed[];//被壓縮的內容,占用N位元組
} quicklistLZF;
quicklist 對比原始兩種編碼的改進
quicklist 同樣采用了 linkedlist 的雙端串列特性,然后 quicklist 中的每個節點又是一個 ziplist,所以quicklist 就是綜合平衡考慮了 linkedlist 容易產生空間碎片的問題和 ziplist 的讀寫性能兩個維度而設計出來的一種資料結構,使用 quicklist 需要注意以下 2 點:
- 如果
ziplist中的entry個數過少,最極端情況就是只有1個entry的壓縮串列,那么此時quicklist就相當于退化成了一個普通的linkedlist, - 如果
ziplist中的entry過多,那么也會導致一次性需要申請的記憶體空間過大(ziplist空間是連續的),而且因為ziplist本身的就是以時間換空間,所以會過多entry也會影響到串列物件的讀寫性能,
ziplist 中的 entry 個數可以通過引數 list-max-ziplist-size 來控制:
list-max-ziplist-size 1
注意:這個引數可以配置正數也可以配置負數,正數表示限制每個節點中的 entry 數量,如果是負數則只能為 -1~-5,其代表的含義如下:
- -1:每個
ziplist最多只能為4KB - -2:每個
ziplist最多只能為8KB - -3:每個
ziplist最多只能為16KB - -4:每個
ziplist最多只能為32KB - -5:每個
ziplist最多只能為64KB
串列物件常用操作命令
- lpush key value1 value2:將一個或者多個
value插入到串列key的頭部,key不存在則創建key(value2在value1之后), - lpushx key value1 value2:將一個或者多個
value插入到串列key的頭部,key不存在則不做任何處理(value2在value1之后), - lpop key:移除并回傳
key值的串列頭元素, - rpush key value1 value2:將一個或者多個
value插入到串列key的尾部,key不存在則創建key(value2在value1之后), - rpushx key value1 vaue2:將一個或者多個
value插入到串列key的尾部,key不存在則不做任何處理(value2在value1之后), - rpop key:移除并回傳串列
key的尾元素, - llen key:回傳串列
key的長度, - lindex key index:回傳串列
key中下標為index的元素,index為正數(從0開始)表示從隊頭開始算,index為負數(從-1開始)則表示從隊尾開始算, - lrange key start stop:回傳串列
key中下標[start,end]之間的元素, - lset key index value:將
value設定到串列key中指定index位置,key不存在或者index超出范圍則會報錯, - ltrim key start end:截取串列中
[start,end]之間的元素,并替換原串列保存,
了解了操作串列物件的常用命令,我們就可以來驗證下前面提到的串列物件的型別和編碼了,在測驗之前為了防止其他 key 值的干擾,我們先執行 flushall 命令清空 Redis 資料庫,
接下來依次輸入命令:
lpush name zhangsantype nameobject encoding name

可以看到,通過 type 命令輸出的是 list,說明當前 name 存的是一個串列物件,并且編碼是 quicklist(示例中用的是 5.0.5 版本),
總結
本文主要介紹了 Redis 中 5 種常用資料型別中的 串列物件,并介紹了底層的存盤結構 quicklist,并分別對舊版本的兩種底層資料 linkedlist 和 ziplist 進行了分析對比得出了為什么 Redis 最終要采用 quicklist 來存盤串列物件,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/250725.html
標籤:其他
上一篇:系列筆記-USYD悉尼大學INFO1110 ED lessons Week1 課件 作業 assignment講解
