Redis List 底層三種資料結構原理剖析
1. Redis List 是什麼
作為 Java 開發者的你,看到這個詞並不陌生。在 Java 開發中幾乎每天都會使用這個資料結構。
Redis 的 List 與 Java 中的 LinkedList 類似,是一種線性的有序結構,可以按照元素被推入列表中的順序來儲存元素,能滿足先進先出的需求,這些元素既可以是文字資料,又可以是二進位制資料。
你可以把他當做佇列、棧來使用。
2. 修煉心法
我叫 Redis,在 C 語言中,並沒有現成的連結串列結構,所以 antirez 為我專門設計了一套實現方式。
關於 List 型別的底層資料結構,可謂英雄輩出,antirez 大佬一直在最佳化,創造了多種資料結構來儲存。
從一開始早期版本使用 linkedlist(雙端列表)和 ziplist(壓縮列表)作為 List 的底層實現,到 Redis 3.2 引入了由 linkedlist + ziplist 組成的 quicklist,再到 7.0 版本的時候使用 listpack 取代 ziplist。
MySQL:“為何弄了這麼多資料結構呀?”
antirez 所做的這一切都是為了在記憶體空間開銷與訪問效能之間做取捨和平衡,跟著我去吃透每個型別的設計思想和不足,你就明白了。
linkedlist(雙端列表)
在 Redis 3.2 版本之前,List 的底層資料結構由 linkedlist 或者 ziplist 實現,優先使用 ziplist 儲存。
當列表物件滿足以下兩個條件的時候,List 將使用 ziplist 儲存,否則使用 linkedlist。
List 的每個元素的佔用的位元組小於 64 位元組。 List 的元素數量小於 512 個。
連結串列的節點使用 adlist.h/listNode
結構來表示。
typedef struct listNode {
// 前驅節點
struct listNode *prev;
// 後驅節點
struct listNode *next;
// 指向節點的值
void *value;
} listNode;
listNode
之間透過 prev 和 next 指標組成雙端連結串列。除此之外,我還提供了 adlist.h/list
結構提供了頭指標 head、尾指標 tail 以及一些實現多型的特定函式。
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;
linkedlist 的結構如圖 2-5 所示。
圖 2-5
Redis 的連結串列實現的特性總結如下。
雙端:連結串列節點帶有 prev 和 next 指標,獲取某個節點的前置節點和後繼節點的複雜度都是 O(1)。 無環:表頭節點的 prev 指標和尾節點的 next 指標都指向 NULL,對連結串列的訪問以 NULL 為結束。 帶表頭指標和表尾指標:透過 list 結構的 head 指標和 tail 指標,程式獲取連結串列的頭節點和尾節點的複雜度為 O(1)。 使用 list 結構的 len 屬性來對記錄節點數量,獲取連結串列中節點數量的複雜度為 O(1)。
MySQL:“看起來沒啥問題呀,為啥還要 ziplist 呢?”
你知道的,我在追求快和節省記憶體的方向上無所不及,有兩個原因導致了 ziplist 的誕生。
普通的 linkedlist 有 prev、next 兩個指標,當儲存資料很小的情況下,指標佔用的空間會超過資料佔用的空間,這就離譜了,是可忍孰不可忍。 linkedlist 是連結串列結構,在記憶體中不是連續的,遍歷的效率低下。
ziplist(壓縮列表)
為了解決上面兩個問題,antirez 創造了 ziplist 壓縮列表,是一種記憶體緊湊的資料結構,佔用一塊連續的記憶體空間,提升記憶體使用率。
當一個列表只有少量資料的時候,並且每個列表項要麼是小整數值,要麼就是長度比較短的字串,那麼我就會使用 ziplist 來做 List 的底層實現。
ziplist 中可以包含多個 entry 節點,每個節點可以存放整數或者字串,結構如圖 2-6 所示。
圖 2-6
zlbytes,佔用 4 個位元組,記錄了整個 ziplist 佔用的總位元組數。 zltail,佔用 4 個位元組,指向最後一個 entry 偏移量,用於快速定位最後一個 entry。 zllen,佔用 2 位元組,記錄 entry 總數。 entry,列表元素。 zlend,ziplist 結束標誌,佔用 1 位元組,值等於 255。
因為 ziplist 頭尾後設資料的大小是固定的,並且在 ziplist 頭部 zllen 記錄了最後一個元素的位置,所以,當在 ziplist 中查詢第一個或最後一個元素的時候,能以 O(1) 時間複雜度找到。
而查詢中間元素時,只能從列表頭或者列表尾遍歷,時間複雜度就是 O(N)。
接下來看真正儲存資料的 entry 結構長啥樣。
圖 2-7
正常來說有三部分構成 <prevlen> <encoding> <entry-data>
。
prevlen
記錄前一個 entry 佔用位元組數,能實現逆序遍歷就是靠這個欄位確定往前移動多少位元組拿到上一個 entry 首地址。
這部分會根據上一個 entry 的長度進行變長編碼(為了節省記憶體操碎了心),變長方式如下。
前一個 entry 的位元組大小小於 254(255 用於 zlend),prevlen 長度為 1 位元組,值等於上一個 entry 的長度。 前一個 entry 的位元組大小大於等於 254,prevlen 佔用 5 位元組,第一個位元組設定為 254 作為一個標識,後面四位元組組成一個 32 位的 int 值,用於存放上一個 entry 的位元組長度。
encoding
簡言之用於表示當前 entry 的型別和長度,當前 entry 的長度和值是根據儲存的是 int 還是 string 以及資料的長度共同來決定。
前兩位用於表示型別,當前兩位值為 “11” 則表示 entry 存放的是 int 型別資料,其他表示儲存的是 string。
entry-data
實際存放資料的區域,需要注意的是,如果 entry 中儲存的是 int 型別,encoding 和 entry-data 會合併到 encoding 中,沒有 entry-data 欄位。
此刻結構就變成了 <prevlen> <encoding>
。
MySQL:“為什麼說 ziplist 省記憶體?”
與 linkedlist 相比,少了 prev、next 指標。 透過 encoding 欄位針對不同編碼來細化儲存,儘可能做到按需分配,當 entry 儲存的是 int 型別時,encoding 和 entry-data 會合併到 encoding ,省掉了 entry-data 欄位。 每個 entry-data 佔據記憶體大小不一樣,為了解決遍歷問題,增加了 prevlen 記錄上一個 entry 長度。遍歷資料時間複雜度是 O(1),但是資料量很小的情況下影響不大。
MySQL:“聽起來很完美,為啥還搞什麼 quicklist ”
既要又要還要的需求是很難實現的,ziplist 節省了記憶體,但是也有不足。
不能儲存過多的元素,否則查詢效能會大大降低,O(N) 時間複雜度。 ziplist 儲存空間是連續的,當插入新的 entry 時,記憶體空間不足就需要重新分配一塊連續的記憶體空間,引發連鎖更新的問題。
連鎖更新
每個 entry 都用 prevlen 記錄了上一個 entry 的長度,從當前 entry B 前面插入一個新的 entry A 時,會導致 B 的 prevlen 改變,也會導致 entry B 大小發生變化。entry B 後一個 entry C 的 prevlen 也需要改變。以此類推,就可能造成了連鎖更新。
圖 2-8
連鎖更新會導致 ziplist 的記憶體空間需要多次重新分配,直接影響 ziplist 的查詢效能。於是乎在 Redis 3.2 版本引入了 quicklist。
quicklist
quicklist 是綜合考慮了時間效率與空間效率引入的新型資料結構。結合了原先 linkedlist 與 ziplist 各自的優勢,本質還是一個連結串列,只不過連結串列的每個節點是一個 ziplist。
資料結構定義在 quicklist.h
檔案中,連結串列由 quicklist
結構體定義,每個節點由 quicklistNode
結構體定義(原始碼版本為 6.2,7.0 版本使用 listpack 取代了 ziplist)。
quicklist 是一個雙向連結串列,所以每個 quicklistNode 都有前序指標(*prev
)、後序指標(*next
)。每個節點是 ziplist,所以還有一個指向 ziplist 的指標 *zl
。
typedef struct quicklistNode {
// 前序節點指標
struct quicklistNode *prev;
// 後序節點指標
struct quicklistNode *next;
// 指向 ziplist 的指標
unsigned char *zl;
// ziplist 位元組大小
unsigned int sz;
// ziplst 元素個數
unsigned int count : 16;
// 編碼格式,1 = RAW 代表未壓縮原生ziplist,2=LZF 壓縮儲存
unsigned int encoding : 2;
// 節點持有的資料型別,預設值 = 2 表示是 ziplist
unsigned int container : 2;
// 節點持有的 ziplist 是否經過解壓, 1 表示已經解壓過,下一次操作需要重新壓縮。
unsigned int recompress : 1;
// ziplist 資料是否可壓縮,太小資料不需要壓縮
unsigned int attempted_compress : 1;
// 預留欄位
unsigned int extra : 10;
} quicklistNode;
quicklist 作為連結串列,定義了 頭、尾指標,用於快速定位表表頭和連結串列尾。
typedef struct quicklist {
// 連結串列頭指標
quicklistNode *head;
// 連結串列尾指標
quicklistNode *tail;
// 所有 ziplist 的總 entry 個數
unsigned long count;
// quicklistNode 個數
unsigned long len;
int fill : QL_FILL_BITS;
unsigned int compress : QL_COMP_BITS;
unsigned int bookmark_count: QL_BM_BITS;
// 柔性陣列,給節點新增標籤,透過名稱定位節點,實現隨機訪問的效果
quicklistBookmark bookmarks[];
} quicklist;
結合 quicklist 和 quicklistNode
定義,quicklist 連結串列結構如下圖所示。
圖 2-9
從結構上看,quicklist 就是 ziplist 的升級版,最佳化的關鍵點在於控制好每個 ziplist 的大小或者元素個數。
quicklistNode 的 ziplist 越小,可能會造成更多的記憶體碎片,極端情況下是每個 ziplist 只有一個 entry,退化成了 linkedlist。 quicklistNode 的 ziplist 過大,極端情況下一個 quicklist 只有一個 ziplist,退化成了 ziplist。連鎖更新的效能問題就會暴露無遺。
合理配置很重要,Redis 提供了 list-max-ziplist-size -2
,
當 list-max-ziplist-size
為負數時表示限制每個 quicklistNode 的 ziplist 的記憶體大小,超過這個大小就會使用 linkedlist 儲存資料,每個值有以下含義:
-5:每個 quicklist 節點上的 ziplist 大小最大 64 kb <--- 正常環境不推薦 -4:每個 quicklist 節點上的 ziplist 大小最大 32 kb <--- 不推薦 -3:每個 quicklist 節點上的 ziplist 大小最大 16 kb <--- 可能不推薦 -2:每個 quicklist 節點上的 ziplist 大小最大 8 kb <--- 不錯 -1:每個 quicklist 節點上的 ziplist 大小最大 4kb <--- 不錯
預設值為 -2,也是官方最推薦的值,當然你可以根據自己的實際情況進行修改。
MySQL:“搞了半天還是沒能解決連鎖更新的問題嘛”
別急,飯要一口口吃,路要一步步走,步子邁大了容易扯著蛋。
ziplist 是緊湊型資料結構,可以有效利用記憶體。但是每個 entry 都用 prevlen
保留了上一個 entry 的長度,所以在插入或者更新時可能會出現連鎖更新影響效率。
於是 antirez 又設計出了“連結串列 + ziplist” 組成的 quicklist 來避免單個 ziplist 過大,降低連鎖更新的影響範圍。
可畢竟還是使用了 ziplist,本質上無法避免連鎖更新的問題,於是乎在 5.0 版本設計出另一個記憶體緊湊型資料結構 listpack,於 7.0 版本替換掉 ziplist。
listpack
出現 listpack 的原因是因為使用者上報了一個 Redis 崩潰的問題,但是 antirez 並沒有找到崩潰的明確原因,猜測可能是 ziplist 結構導致的連鎖更新導致的,於是就想設計一種簡單、高效的資料結構來替換 ziplist 這個資料結構。
MySQL:“listpack 是啥?”
listpack 也是一種緊湊型資料結構,用一塊連續的記憶體空間來儲存資料,並且使用多種編碼方式來表示不同長度的資料來節省記憶體空間。
原始碼檔案 listpack.h
對 listpack 的解釋:A lists of strings serialization format,意思是一種字串列表的序列化格式,可以把字串列表進行序列化儲存,可以儲存字串或者整形數字。
先看 listpack 的整體結構。
圖 2-10
一共四部分組成,tot-bytes、num-elements、elements、listpack-end-byte。
tot-bytes,也就是 total bytes,佔用 4 位元組,記錄 listpack 佔用的總位元組數。 num-elements,佔用 2 位元組,記錄 listpack elements 元素個數。 elements,listpack 元素,儲存資料的部分。 listpack-end-byte,結束標誌,佔用 1 位元組,值固定為 255。
MySQL:“好傢伙,這跟 ziplist 有啥區別?別以為換了個名字,換個馬甲我就不認識了”
聽我說完!確實有點像,listpack 也是由後設資料和資料自身組成。最大的區別是 elements 部分,為了解決 ziplist 連鎖更新的問題,element 不再像 ziplist 的 entry 儲存前一項的長度。
圖 2-11
encoding-type,元素的編碼型別,會不同長度的整數和字串編碼。 element-data,實際存放的資料。 element-tot-len,encoding-type + element-data 的總長度,不包含自己的長度。
每個 element 只記錄自己的長度,不像 ziplist 的 entry,記錄上一項的長度。當修改或者新增元素的時候,不會影響後續 element 的長度變化,解決了連鎖更新的問題。
從 linkedlist、 ziplist 到“連結串列 + ziplist” 構成的 quicklist,再到 listpack 結構。可以看到,設計的初衷都是能夠高效的使用記憶體,同時避免效能下降。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024922/viewspace-2938383/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Redis資料結構SortedSet底層原理詳解Redis資料結構
- Redis - 底層資料結構Redis資料結構
- Redis(二)--- Redis的底層資料結構Redis資料結構
- Redis 概念以及底層資料結構Redis資料結構
- 【Redis 系列】redis 學習十五,redis sds資料結構和底層設計原理Redis資料結構
- 深入瞭解Redis底層資料結構Redis資料結構
- Redis底層資料結構——壓縮列表Redis資料結構
- Redis基本資料型別底層資料結構Redis資料型別資料結構
- 【redis】-- 資料結構及底層編碼篇Redis資料結構
- Redis - 資料型別對映底層結構Redis資料型別
- Redis學習筆記(二)redis 底層資料結構Redis筆記資料結構
- HashMap原理底層剖析HashMap
- 聊一聊redis十種資料型別及底層原理Redis資料型別
- Redis原始碼分析-底層資料結構盤點Redis原始碼資料結構
- 《閒扯Redis五》List資料型別底層之quicklistRedis資料型別UI
- 【Mysql】索引底層資料結構MySql索引資料結構
- Redis(一):基本資料型別與底層儲存結構Redis資料型別
- Redis的ZSet底層資料結構,ZSet型別全面解析Redis資料結構型別
- Redis系列(一)底層資料結構之簡單動態字串Redis資料結構字串
- 深度剖析Spring Cloud底層原理SpringCloud
- 深入剖析Redis系列(八) - Redis資料結構之集合Redis資料結構
- 深入剖析Redis系列(五) - Redis資料結構之字串Redis資料結構字串
- 深入剖析Redis系列(七) - Redis資料結構之列表Redis資料結構
- Redis資料結構詳解之List(二)Redis資料結構
- Redis資料結構:List型別全面解析Redis資料結構型別
- Java 的 ArrayList 的底層資料結構Java資料結構
- HashMap底層資料結構原始碼解析HashMap資料結構原始碼
- [Redis 系列]redis 學習三,redis 資料結構之 string 和 list 基本使用及熟悉Redis資料結構
- 【Redis 系列】redis 學習三,redis 資料結構之 string 和 list 基本使用及熟悉Redis資料結構
- ArrayList 從原始碼角度剖析底層原理原始碼
- 深入剖析Redis系列(六) - Redis資料結構之雜湊Redis資料結構
- Redis 的五種資料結構Redis資料結構
- 《閒扯Redis七》Redis字典結構的底層實現Redis
- Redis(三)--- Redis的五大資料型別的底層實現Redis大資料資料型別
- 深入理解MySQL索引底層資料結構MySql索引資料結構
- 資料型別與底層原理資料型別
- 一文讀懂Redis常見物件型別的底層資料結構Redis物件型別資料結構
- HashMap的底層結構、原理、擴容機制HashMap