Redis进阶-List底层数据结构精讲
文章目錄
- Pre
- list 列表
- 隊列 O(1)
- 棧 O(1)
- 查詢 O(n)
- 快速列表 quicklist
- 壓縮列表 ziplist
- ziplist 源碼
- entry
- 增加元素
- 快速列表 quicklist
- ziplist 存多少元素?
- 壓縮深度
- 延伸
Pre
Redis進階-核心數據結構進階實戰
Algorithms_基礎數據結構(03)_線性表之鏈表_雙向鏈表
Redis 有 5 種基礎數據結構,分別為:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合) 。
Redis 所有的數據結構都是以唯一的key 字符串作為名稱,然后通過這個唯一 key 值來獲取相應的 value 數據。不同類型的數據結構的差異就在于 value 的結構不一樣。
list 列表
-
Redis 的列表相當于 Java 語言里面的 LinkedList,是鏈表而不是數組 。
這意味著list 的插入和刪除操作非常快,時間復雜度為 O(1),但是查找數據很慢,時間復雜度為 O(n) 。
-
當列表彈出了最后一個元素之后,該數據結構自動被刪除,內存被回收。
-
Redis 的列表結構常用來做異步隊列使用
將需要延后處理的任務結構體序列化成字符串塞進 Redis 的列表,另一個線程從這個列表中輪詢數據進行處理
隊列 O(1)
右邊進左邊出:隊列
192.168.18.131:8001> rpush artisan art1 art2 art3 art4 (integer) 4 192.168.18.131:8001> llen artisan (integer) 4 192.168.18.131:8001> LRANGE artisan 0 999 1) "art1" 2) "art2" 3) "art3" 4) "art4" 192.168.18.131:8001> LPOP artisan "art1" 192.168.18.131:8001> LPOP artisan "art2" 192.168.18.131:8001> LPOP artisan "art3" 192.168.18.131:8001> LPOP artisan "art4" 192.168.18.131:8001> LPOP artisan (nil) 192.168.18.131:8001>當然了,你也可以左邊進,右邊出,保證FIFO就行。
除了rpush 和 lpop, 還可以使用 lpush 和 rpop 結合使用,效果是一樣的。
棧 O(1)
右邊進右邊出:棧
192.168.18.131:8001> rpush artisan art1 art2 art3 art4 (integer) 4 192.168.18.131:8001> RPOP artisan "art4" 192.168.18.131:8001> RPOP artisan "art3" 192.168.18.131:8001> RPOP artisan "art2" 192.168.18.131:8001> RPOP artisan "art1" 192.168.18.131:8001> RPOP artisan (nil) 192.168.18.131:8001>查詢 O(n)
lindex & ltrim
-
lindex 相當于 Java 鏈表的 get(int index)方法,它需要對鏈表進行遍歷,性能隨著參數index 增大而變差.
-
ltrim : 兩個參數 start_index 和 end_index 定義了一個區間,在這個區間內的值, ltrim 要保留,區間之外統統砍掉。
-
我們可以通過 ltrim 來實現一個定長的鏈表,這一點非常有用。
-
index 可以為負數,index=-1 表示倒數第一個元素,同樣 index=-2 表示倒數第二個元素。
快速列表 quicklist
Redis 底層存儲的還不是一個簡單的 linkedlist,而是稱之為快速鏈表 quicklist 的一個結構。
-
首先在列表元素較少的情況下會使用一塊連續的內存存儲,這個結構是 ziplist ,即壓縮列表 . 它將所有的元素緊挨著一起存儲,分配的是一塊連續的內存
-
當數據量比較多才會改成 quicklist.
因為普通的鏈表需要的附加指針空間太大,會比較浪費空間,而且會加重內存的碎片化 .
比如這個列表里存的只是 int 類型的數據,結構上還需要兩個額外的指針 prev 和 next 。所以 Redis 將鏈表和 ziplist 結合起來組成了 quicklist。也就是將多個ziplist 使用雙向指針串起來使用。這樣既滿足了快速的插入刪除性能,又不會出現太大的空間冗余。
壓縮列表 ziplist
Redis 為了節約內存空間使用,zset 和 hash 容器對象在元素個數較少的時候,采用壓縮列表 (ziplist) 進行存儲。
壓縮列表是一塊連續的內存空間,元素之間緊挨著存儲,沒有任何冗余空隙。
使用DEBUG OBJECT 查看內部存儲結構
192.168.18.131:8001> ZADD artisan 1.0 java 2.0 go 3.0 python (integer) 3 192.168.18.131:8001> DEBUG OBJECT artisan # 查看內部存儲結構 Value at:0x7f131e2ba5d0 refcount:1 encoding:ziplist serializedlength:36 lru:9895943 lru_seconds_idle:6 192.168.18.131:8001> 192.168.18.131:8001> HMSET artisan2 name ysw sex male -> Redirected to slot [6066] located at 192.168.18.132:8002 OK 192.168.18.132:8002> DEBUG OBJECT artisan2 Value at:0x7fb74eaba5f0 refcount:1 encoding:ziplist serializedlength:34 lru:9896028 lru_seconds_idle:9 192.168.18.132:8002>觀察 debug object 輸出的 encoding 字段都是 ziplist,這就表示內部采用壓縮列表結構進行存儲。
ziplist 源碼
struct ziplist<T> {int32 zlbytes; // 整個壓縮列表占用字節數int32 zltail_offset; // 最后一個元素距離壓縮列表起始位置的偏移量,用于快速定位到最后一個節點int16 zllength; // 元素個數T[] entries; // 元素內容列表,挨個挨個緊湊存儲int8 zlend; // 標志壓縮列表的結束,值恒為 0xFF }壓縮列表為了支持雙向遍歷,所以才會有 ztail_offset 這個字段,用來快速定位到最后一 個元素,然后倒著遍歷。
entry
entry 塊隨著容納的元素類型不同,也會有不一樣的結構
struct entry {int<var> prevlen; // 前一個 entry 的字節長度int<var> encoding; // 元素類型編碼optional byte[] content; // 元素內容 }它的 prevlen 字段表示前一個 entry 的字節長度,當壓縮列表倒著遍歷時,需要通過這個字段來快速定位到下一個元素的位置。
-
它是一個變長的整數,當字符串長度小于254(0xFE) 時,使用一個字節表示;
-
如果達到或超出 254(0xFE) 那就使用 5 個字節來表示。
-
第一個字節是 0xFE(254),剩余四個字節表示字符串長度。
用 5 個字節來表示字符串長度,是不是太浪費了?
我們可以算一下,當字符串長度比較長的時候,其實 5個字節也只占用了不到(5/(254+5))<2%的空間。
encoding 字段存儲了元素內容的編碼類型信息,ziplist 通過這個字段來決定后面的content 內容的形式。
增加元素
因為 ziplist 都是緊湊存儲,沒有冗余空間 (對比一下 Redis 的字符串結構)。意味著插入一個新的元素就需要調用 realloc 擴展內存。
取決于內存分配器算法和當前的 ziplist 內存大小,realloc 可能會重新分配新的內存空間,并將之前的內容一次性拷貝到新的地址,也可能在原有的地址上進行擴展,這時就不需要進行舊內容的內存拷貝。
如果 ziplist 占據內存太大,重新分配內存和拷貝內存就會有很大的消耗。所以 ziplist不適合存儲大型字符串,存儲的元素也不宜過多。
快速列表 quicklist
Redis 早期版本存儲 list 列表數據結構使用的是壓縮列表 ziplist 和普通的雙向鏈表linkedlist,也就是元素少時用 ziplist,元素多時用 linkedlist。
// 鏈表的節點 struct listNode<T> {listNode* prev;listNode* next;T value; } // 鏈表 struct list {listNode *head;listNode *tail;long length; }考慮到鏈表的附加空間相對太高,prev 和 next 指針就要占去 16 個字節 (64bit 系統的指針是 8 個字節),另外每個節點的內存都是單獨分配,會加劇內存的碎片化,影響內存管理效率。后續版本對列表數據結構進行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。
192.168.18.132:8002> RPUSH test artisan1 artisan2 artisan3 (integer) 3 192.168.18.132:8002> DEBUG OBJECT test Value at:0x7fb74eaba610 refcount:1 encoding:quicklist serializedlength:36 lru:9897276 lru_seconds_idle:4 ql_nodes:1 ql_avg_node:3.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:41 192.168.18.132:8002>觀察上面輸出字段 encoding 的值。quicklist 是 ziplist 和 linkedlist 的混合體,它將 linkedlist 按段切分,每一段使用 ziplist 來緊湊存儲,多個 ziplist 之間使用雙向指針串接起來。
struct ziplist {... } struct ziplist_compressed {int32 size;byte[] compressed_data; }struct quicklistNode {quicklistNode* prev;quicklistNode* next;ziplist* zl; // 指向壓縮列表int32 size; // ziplist 的字節總數int16 count; // ziplist 中的元素數量int2 encoding; // 存儲形式 2bit,原生字節數組還是 LZF 壓縮存儲... } struct quicklist {quicklistNode* head;quicklistNode* tail;long count; // 元素總數int nodes; // ziplist 節點的個數int compressDepth; // LZF 算法壓縮深度... }上述代碼簡單地表示了 quicklist 的大致結構。為了進一步節約空間,Redis 還會對ziplist 進行壓縮存儲,使用 LZF 算法壓縮,可以選擇壓縮深度。
ziplist 存多少元素?
quicklist 內部默認單個 ziplist 長度為 8k 字節,超出了這個字節數,就會新起一個ziplist。
ziplist 的長度由配置參數 list-max-ziplist-size 決定。
壓縮深度
quicklist 默認的壓縮深度是 0,也就是不壓縮。壓縮的實際深度由配置參數 list-compress-depth 決定.
為了支持快速的 push/pop 操作,quicklist 的首尾兩個 ziplist 不壓縮,此時深度就是 1。
如果深度為 2,就表示 quicklist 的首尾第一個 ziplist 以及首尾第二個 ziplist 都不壓縮。
延伸
參考:《Redis深度歷險:核心原理和應用實踐》
ziplist、linkedlist 和 quicklist 的性能對比
總結
以上是生活随笔為你收集整理的Redis进阶-List底层数据结构精讲的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis进阶-JedisCluster
- 下一篇: Redis进阶-string底层数据结构