【Redis系列3】Redis列表物件之linkedlist(雙端列表)和ziplist(壓縮列表)及quicklick(快速列表)實現原理分析
Redis列表物件之linkedlist和ziplist實現原理分析
前言
上一篇我們分析了字串物件的底層儲存結構SDS,那麼這一篇我們繼續分析Redis中5種常用資料型別的第2種基本資料類列表物件
的底層儲存結構。
列表物件
Redis3.2之前,列表物件其底層儲存結構可以有兩種,即:linkedlist
和ziplist
,而在Redis 3.2之後,列表物件底層儲存結構優化成為了另一種:quicklist
。而quicklist
可以認為是linkedlist
和ziplist
的結合體。
列表內部使用哪一種型別也是通過編碼來進行區分:
編碼屬性 | 描述 | 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。
所以,同樣的,在Redis3.2之前我們可以得到如下簡圖:
PS:想要詳細瞭解dictEntry
和redisObject
物件以及編碼相關知識的的可以點選這裡。
ziplist
ziplist
是為了節省記憶體而開發的一種壓縮列表資料結構,後面講述的雜湊資料型別底層也用到了ziplist
。
ziplist
是由一系列特殊編碼的連續記憶體塊組成的順序型資料結構,一個ziplist
可以包含任意多個entry
,而每一個entry
又可以儲存一個位元組陣列或者一個整數值。
ziplist和linkedlist最大的區別是ziplist不儲存指向上一個節點和下一個節點的指標,儲存的是上一個節點的長度和當前節點的長度,犧牲了部分讀寫效能來換取搞笑的記憶體利用率,是一種時間換空間的思想。
ziplist
適用於欄位個數少和欄位值少的場景。
ziplist儲存結構
ziplist
的組成結構為:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
屬性 | 型別 | 長度 | 說明 |
---|---|---|---|
zlbytes | uint32_t | 4位元組 | 記錄壓縮列表佔用記憶體位元組數(包括本身所佔用的4個位元組) |
zltail | uint32_t | 4位元組 | 記錄壓縮列表尾節點距離壓縮列表的起始地址有多少個位元組(通過這個值可以計算出尾節點的地址) |
zllen | uint16_t | 2位元組 | 記錄壓縮列表中包含的節點數量,當列表值超過可以儲存的最大值(65535)時,次值固定儲存216-1(65535),因此此時需要遍歷整個壓縮列表才能計算出真實節點數 |
entry | 列表節點 | - | 壓縮列表中的各個節點,長度由儲存的實際資料決定 |
zlend | uint8_t | 1位元組 | 特殊字元0xFF(十進位制255),用來標記壓縮列表的末端(其他正常的節點沒有被標記為255的,因為255用來標識末尾,後面可以看到,正常節點都是標記為254) |
entry儲存結構
ziplist
中的每個 entry
都以包含兩段資訊的後設資料作為字首,的組成結構為:
<prevlen> <encoding> <entry-data>
prevlen
prevlen
屬性儲存了前一個entry
的長度,以便能夠從後到前遍歷列表。 prevlen
的長度可能是1位元組也可能是5位元組:
- 當連結串列的前一個
entry
佔用位元組數小於254,此時prevlen
只用1個位元組進行表示。
<prevlen from 0 to 253> <encoding> <entry>
- 當連結串列的前一個
entry
佔用位元組數大於等於254,此時prevlen
用5個位元組來表示,其中第1個位元組的值是254(相當於是一個標記,代表後面跟了一個更大的值),後面4個位元組才是真正儲存前一個entry
的佔用位元組數。
0xFE <4 bytes unsigned little endian prevlen> <encoding> <entry>
PS:1個位元組完全你能儲存255,之所以只取到254是因為zlend
就是固定的255,所以255這個數要用來判斷是否是ziplist
的結尾。
encoding
encoding
屬性儲存了當前entry
所儲存資料的型別以及長度。前面我們提到,每一個entry
中可以儲存位元組陣列和整數,而encoding
屬性的第1個位元組就是用來確定當前entry
儲存的是整數還是位元組陣列。當儲存整數時,第1個位元組的前兩位總是11,而儲存位元組陣列時,則可能是00、01和10。
- 當儲存整數時,第1個位元組的前2位固定為11,第3和第4位用於指定將在這個頭之後儲存哪種型別的整數:
編碼 | 長度 | entry儲存的資料 |
---|---|---|
11000000 | int16_t型別整數 | |
11010000 | int32_t型別整數 | |
11100000 | int64_t型別整數 | |
11110000 | 24位有符號整數 | |
11111110 | 8位有符號整數 | |
1111xxxx | xxxx代表區間0001-1101,儲存了一個介於0-12之間的整數,此時無需entry-data屬性 |
PS:xxxx四位編碼範圍是0000-1111,但是0000,1111和1110已經被佔用了,所以實際上的範圍是0001-1101,此時能儲存資料1-13,再減去1之後範圍就是0-12(為什麼要減1這個我沒想明白)。
編碼 | 長度 | entry儲存的資料 |
---|---|---|
00pppppp | 1位元組 | 長度小於等於63位元組(6位)的位元組陣列 |
01pppppp qqqqqqqq | 2位元組 | 長度小於等於16383位元組(14位)的位元組陣列 |
10000000 qqqqqqqq rrrrrrrr ssssssss tttttttt | 5位元組 | 長度小於等於232-1位元組(32位)的位元組陣列,其中第1個位元組的後6位設定為0,暫時沒有用到,後面的32位(4個位元組)儲存了資料 |
entry-data
entry-data
:具體資料。當儲存小整數時,encoding
就是資料本身,此時沒有entry-data
部分,沒有entry-data
部分之後的ziplist
結構如下:
<prevlen> <encoding>
壓縮列表中entry
的資料結構定義如下(原始碼ziplist.c內),當然這個程式碼註釋寫了實際儲存並沒有用到這個結構,這個結構只是用來接收資料,所以瞭解一下就可以了:
typedef struct zlentry {
unsigned int prevrawlensize;//儲存prevrawlen所佔用的位元組數
unsigned int prevrawlen;//儲存上一個連結串列節點需要的位元組數
unsigned int lensize;//儲存len所佔用的位元組數
unsigned int len;//儲存連結串列當前節點的位元組數
unsigned int headersize;//當前連結串列節點的頭部大小(prevrawlensize + lensize)即非資料域的大小
unsigned char encoding;//編碼方式
unsigned char *p;//指向當前節點的起始位置(因為列表內的資料也是一個字串物件)
} zlentry;
ziplist資料示例
上面講解了這麼多,聽起來非常複雜,下面我們就通過一個ziplist
儲存整數為例子來分析一下。
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "5" end
1、第一組4個位元組代表zlbytes,0f轉成二進位制就是1111也就是15,也就是這整個ziplist
長度是15個位元組。
2、第二組4個位元組zltail,0c轉成二進位制就是1100也就是12,就是說[02 f6]這個尾節點距離起始位置有12個位元組。
3、第三組2個位元組就是記錄了當前ziplist
中entry
的數量,02轉成二進位制就是10,也就是說當前ziplist
有2個節點
4、[00 f3]就是第一個entry
,f3轉成二進位制就是11110011,剛好對應了編碼1111xxxx,所以後面四位就是儲存了一個0-12的整數。0011轉成十進位制就是3,減去1得到2,所以第一個entry
儲存的資料就是2。後面[02 f6]一樣的演算法可以得出就是5。
5、最後一組1個位元組[ff]轉成二進位制就是11111111,代表這是整個ziplist
的結尾。
ziplist連鎖更新問題
前面提到entry
中的prevlen
屬性可能是1個位元組也可能是5個位元組,我們設想這麼一種場景:假設一個ziplist
中,連續多個entry
的長度都是一個接近但是又不到254的值(介於250~253之間),假如這時候新增一個entry1
長度增加到大於254個位元組,那麼此時entry2
的prelen
屬性就必須要由1個位元組變為5個位元組,也就是需要執行空間重分配,而此時因為entry2
長度也增加了4個位元組,又大於254個位元組了了,那麼entry3
的prelen
屬性也會被迫變為5個位元組,依此類推,這種產生連續多次空間重分配的現象就稱之為連鎖更新。
PS:不僅僅是新增節點,執行刪除節點操作同樣可能會發生連鎖更新現象。
linkedlist和ziplist的選擇
在Redis3.2之前,linkedlist
和ziplist
兩種編碼可以進選擇切換,如果需要列表使用ziplist
編碼進行儲存,則必須滿足以下兩個條件:
- 列表物件儲存的所有字串元素的長度都小於64位元組。
- 列表物件爆粗的元素數量小於512個。
一旦不滿足這兩個條件的任意一個,則會使用linkedlist
編碼進行儲存。
PS:這兩個條件可以通過引數list-max-ziplist-value
和list-max-ziplist-entries
進行修改。
quicklist
在Redis3.2之後,統一用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;
根據這兩個結構,我們可以得到Redis3.2之後的列表物件的一個簡圖:
quicklist的compress屬性
compress表示壓縮深度,可以通過引數list-compress-depth
控制:
- 0:不壓縮(預設值)
- 1:首尾第1個元素不壓縮
- 2:首位前2個元素不壓縮
- 3:首尾前3個元素不壓縮
- 以此類推
PS:之所以採取這種方式去控制是因為很多場景都是兩端的元素訪問率較高,而中間元素訪問率相對較低。
quicklistNode的zl指標
zl指標預設指向了ziplist
,sz屬性記錄了當前ziplist
佔用的位元組,不過這僅僅限於當前節點沒有被壓縮(LZF壓縮演算法)的情況,如果當前節點被壓縮了,那麼zl指標會指向另一個物件quicklistLZF
,quicklistLZF
是一個4+N位元組的結構:
typedef struct quicklistLZF {
unsigned int sz;// LZF大小
char compressed[];//被壓縮的內容
} quicklistLZF;
quicklist對比原始兩種列表的改進
quicklist
同樣採用了linkedlist
的雙端列表特性,然後quicklist
中的每個節點又是一個ziplist
,所以quicklist
就是綜合平衡考慮了空間碎片和讀寫效能兩個維度。使用quicklist
需要注意以下2點:
- 1、如果
ziplist
中的entry
個數過少,極端情況就是隻有1個entry
,此時就相當於退化成了一個普通的linkedlist
。 - 2、如果
ziplist
中的entry
過多,那麼也會導致一次性需要申請的記憶體空間過大,而且因為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
總結
本文主要介紹了Redis中5種常用資料型別中的列表型別
底層的儲存結構,並分別對其兩種底層資料linkedlist
和ziplist
進行了分析對比,最後分析了Redis3.2之後的底層資料型別quicklist
的儲存原理。
下一篇,我們將分析Redis中5種常用資料型別中的第3種雜湊物件的底層儲存結構。
請關注我,和孤狼一起學習進步。
相關文章
- Redis核心原理與實踐--列表實現原理之ziplistRedis
- Redis 設計與實現 5:壓縮列表Redis
- Redis資料結構三之壓縮列表Redis資料結構
- 圖解Redis之資料結構篇——壓縮列表圖解Redis資料結構
- Redis底層資料結構——壓縮列表Redis資料結構
- Redis核心原理與實踐--列表實現原理之quicklist結構RedisUI
- Redis資料結構—整數集合與壓縮列表Redis資料結構
- Redis常見面試題:ZSet底層資料結構,SDS、壓縮列表ZipList、跳錶SkipListRedis面試題資料結構
- redis:新增redis到服務列表Redis
- Redis的列表型別(List)Redis型別
- 深入剖析Redis系列(七) - Redis資料結構之列表Redis資料結構
- 5分鐘瞭解Redis的內部實現快速列表(quicklist)RedisUI
- PHP+Redis實戰教程(3):list列表型別PHPRedis型別
- Bootstrap雙列表boot
- Redis 入門 - 2(雜湊 + 列表)Redis
- React-列表元件(通知列表、私信列表、虛擬列表)React元件
- redis之列表型別(list)——佇列和棧簡單實現Redis型別佇列
- SQL獲取時間列表及月份列表SQL
- Redis五大資料型別之 List(列表)Redis大資料資料型別
- 【最完整系列】Redis-結構篇-跳躍列表Redis
- redis原始碼分析(八)、redis資料結構之壓縮ziplist--------ziplist.c ziplist.h學習筆記Redis原始碼資料結構筆記
- Redis 的基礎資料結構(二) 整數集合、跳躍表、壓縮列表Redis資料結構
- SharePoint REST API - 列表和列表項RESTAPI
- PbootCMS內容和列表頁呼叫tags列表boot
- Python基礎-列表操作(2):列表的遍歷和數字列表Python
- Redis 6.0 訪問控制列表ACL說明Redis
- Python元組、列表、集合及列表去重操作Python
- Python基礎-列表及列表解析小結Python
- 面試官:Redis中列表的內部實現方式是什麼?面試Redis
- 列表
- 交替顏色列表實現
- list列表運算子,列表元素的遍歷,列表的方法,生成列表,巢狀的列表|python自學筆記(四)巢狀Python筆記
- Bootstrap系列 -- 6. 列表boot
- 列表元件抽象(4):滾動列表及分頁說明元件抽象
- Redis 4.0.10 中文文件(完整的命令列表)Redis命令列
- RecyclerView進階(一)RecyclerView實現雙列表聯動View
- Python函數語言程式設計系列009:惰性列表之常規列表Python函數程式設計
- 列表及相關操作