Redis Ziplist 概述
https://www.jianshu.com/p/37c49b4e3bd0
壓縮列表
壓縮列表(ziplist)是列表鍵和雜湊鍵的底層實現之一。
當一個列表鍵只包含少量列表項, 並且每個列表項要麼就是小整數值, 要麼就是長度比較短的字串, 那麼 Redis 就會使用壓縮列表來做列表鍵的底層實現。
比如說, 執行以下命令將建立一個壓縮列表實現的列表鍵:
redis> RPUSH lst 1 3 5 10086 "hello" "world"
(integer) 6
redis> OBJECT ENCODING lst
"ziplist"
因為列表鍵裡面包含的都是 1 、 3 、 5 、 10086 這樣的小整數值, 以及 "hello" 、 "world" 這樣的短字串。
另外, 當一個雜湊鍵只包含少量鍵值對, 並且每個鍵值對的鍵和值要麼就是小整數值, 要麼就是長度比較短的字串, 那麼 Redis 就會使用壓縮列表來做雜湊鍵的底層實現。
舉個例子, 執行以下命令將建立一個壓縮列表實現的雜湊鍵:
redis> HMSET profile "name" "Jack" "age" 28 "job" "Programmer"
OK
redis> OBJECT ENCODING profile
"ziplist"
因為雜湊鍵裡面包含的所有鍵和值都是小整數值或者短字串。
本章將對壓縮列表的定義以及相關操作進行詳細的介紹。
壓縮列表的構成
壓縮列表是 Redis 為了節約記憶體而開發的, 由一系列特殊編碼的連續記憶體塊組成的順序型(sequential)資料結構。
一個壓縮列表可以包含任意多個節點(entry), 每個節點可以儲存一個位元組陣列或者一個整數值。
圖 7-1 展示了壓縮列表的各個組成部分, 表 7-1 則記錄了各個組成部分的型別、長度、以及用途。
表 7-1 壓縮列表各個組成部分的詳細說明
屬性 | 型別 | 長度 | 用途 |
---|---|---|---|
zlbytes | uint32_t | 4 位元組 | 記錄整個壓縮列表佔用的記憶體位元組數:在對壓縮列表進行記憶體重分配, 或者計算 zlend 的位置時使用。 |
zltail | uint32_t | 4 位元組 | 記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少位元組: 通過這個偏移量,程式無須遍歷整個壓縮列表就可以確定表尾節點的地址。 |
zllen | uint16_t | 2 位元組 | 記錄了壓縮列表包含的節點數量: 當這個屬性的值小於 UINT16_MAX (65535)時, 這個屬性的值就是壓縮列表包含節點的數量; 當這個值等於 UINT16_MAX 時, 節點的真實數量需要遍歷整個壓縮列表才能計算得出。 |
entryX | 列表節點 | 不定 | 壓縮列表包含的各個節點,節點的長度由節點儲存的內容決定。 |
zlend | uint8_t | 1 位元組 | 特殊值 0xFF (十進位制 255 ),用於標記壓縮列表的末端。 |
圖 7-2 展示了一個壓縮列表示例:
- 列表 zlbytes 屬性的值為 0x50 (十進位制 80), 表示壓縮列表的總長為 80 位元組。
- 列表 zltail 屬性的值為 0x3c (十進位制 60), 這表示如果我們有一個指向壓縮列表起始地址的指標 p , 那麼只要用指標 p 加上偏移量 60 , 就可以計算出表尾節點 entry3 的地址。
- 列表 zllen 屬性的值為 0x3 (十進位制 3), 表示壓縮列表包含三個節點。
圖 7-3 展示了另一個壓縮列表示例:
- 列表 zlbytes 屬性的值為 0xd2 (十進位制 210), 表示壓縮列表的總長為 210 位元組。
- 列表 zltail 屬性的值為 0xb3 (十進位制 179), 這表示如果我們有一個指向壓縮列表起始地址的指標 p , 那麼只要用指標 p 加上偏移量 179 , 就可以計算出表尾節點 entry5 的地址。
- 列表 zllen 屬性的值為 0x5 (十進位制 5), 表示壓縮列表包含五個節點。
連鎖更新
前面說過, 每個節點的 previous_entry_length 屬性都記錄了前一個節點的長度:
如果前一節點的長度小於 254 位元組, 那麼 previous_entry_length 屬性需要用 1 位元組長的空間來儲存這個長度值。
如果前一節點的長度大於等於 254 位元組, 那麼 previous_entry_length 屬性需要用 5 位元組長的空間來儲存這個長度值。
現在, 考慮這樣一種情況: 在一個壓縮列表中, 有多個連續的、長度介於 250 位元組到 253 位元組之間的節點 e1 至 eN , 如圖 7-11 所示。
因為 e1 至 eN 的所有節點的長度都小於 254 位元組, 所以記錄這些節點的長度只需要 1 位元組長的 previous_entry_length 屬性, 換句話說, e1 至 eN 的所有節點的 previous_entry_length 屬性都是 1 位元組長的。
這時, 如果我們將一個長度大於等於 254 位元組的新節點 new 設定為壓縮列表的表頭節點, 那麼 new 將成為 e1 的前置節點, 如圖 7-12 所示。
因為 e1 的 previous_entry_length 屬性僅長 1 位元組, 它沒辦法儲存新節點 new 的長度, 所以程式將對壓縮列表執行空間重分配操作, 並將 e1 節點的 previous_entry_length 屬性從原來的 1 位元組長擴充套件為 5 位元組長。
現在, 麻煩的事情來了 —— e1 原本的長度介於 250 位元組至 253 位元組之間, 在為 previous_entry_length 屬性新增四個位元組的空間之後, e1 的長度就變成了介於 254 位元組至 257 位元組之間, 而這種長度使用 1 位元組長的 previous_entry_length 屬性是沒辦法儲存的。
因此, 為了讓 e2 的 previous_entry_length 屬性可以記錄下 e1 的長度, 程式需要再次對壓縮列表執行空間重分配操作, 並將 e2 節點的 previous_entry_length 屬性從原來的 1 位元組長擴充套件為 5 位元組長。
正如擴充套件 e1 引發了對 e2 的擴充套件一樣, 擴充套件 e2 也會引發對 e3 的擴充套件, 而擴充套件 e3 又會引發對 e4 的擴充套件……為了讓每個節點的 previous_entry_length 屬性都符合壓縮列表對節點的要求, 程式需要不斷地對壓縮列表執行空間重分配操作, 直到 eN 為止。
Redis 將這種在特殊情況下產生的連續多次空間擴充套件操作稱之為“連鎖更新”(cascade update), 圖 7-13 展示了這一過程。
除了新增新節點可能會引發連鎖更新之外, 刪除節點也可能會引發連鎖更新。
考慮圖 7-14 所示的壓縮列表, 如果 e1 至 eN 都是大小介於 250 位元組至 253 位元組的節點, big 節點的長度大於等於 254 位元組(需要 5 位元組的 previous_entry_length 來儲存), 而 small 節點的長度小於 254 位元組(只需要 1 位元組的 previous_entry_length 來儲存), 那麼當我們將 small 節點從壓縮列表中刪除之後, 為了讓 e1 的 previous_entry_length 屬性可以記錄 big 節點的長度, 程式將擴充套件 e1 的空間, 並由此引發之後的連鎖更新。
因為連鎖更新在最壞情況下需要對壓縮列表執行 N 次空間重分配操作, 而每次空間重分配的最壞複雜度為 O(N) , 所以連鎖更新的最壞複雜度為 O(N^2) 。
要注意的是, 儘管連鎖更新的複雜度較高, 但它真正造成效能問題的機率是很低的:
- 首先, 壓縮列表裡要恰好有多個連續的、長度介於 250 位元組至 253 位元組之間的節點, 連鎖更新才有可能被引發, 在實際中, 這種情況並不多見;
- 其次, 即使出現連鎖更新, 但只要被更新的節點數量不多, 就不會對效能造成任何影響: 比如說, 對三五個節點進行連鎖更新是絕對不會影響效能的;
因為以上原因, ziplistPush 等命令的平均複雜度僅為 O(N) , 在實際中, 我們可以放心地使用這些函式, 而不必擔心連鎖更新會影響壓縮列表的效能。
壓縮列表 API
表 7-4 列出了所有用於操作壓縮列表的 API 。
表 7-4 壓縮列表 API
函式 | 作用 | 演算法複雜度 |
---|---|---|
ziplistNew | 建立一個新的壓縮列表。 | O(1) |
ziplistPush | 建立一個包含給定值的新節點, 並將這個新節點新增到壓縮列表的表頭或者表尾。 | 平均 O(N) ,最壞 O(N^2) 。 |
ziplistInsert | 將包含給定值的新節點插入到給定節點之後。 | 平均 O(N) ,最壞 O(N^2) 。 |
ziplistIndex | 返回壓縮列表給定索引上的節點。 | O(N) |
ziplistFind | 在壓縮列表中查詢並返回包含了給定值的節點。 | 因為節點的值可能是一個位元組陣列, 所以檢查節點值和給定值是否相同的複雜度為 O(N) , 而查詢整個列表的複雜度則為 O(N^2) 。 |
ziplistNext | 返回給定節點的下一個節點。 | O(1) |
ziplistPrev | 返回給定節點的前一個節點。 | O(1) |
ziplistGet | 獲取給定節點所儲存的值。 | O(1) |
ziplistDelete | 從壓縮列表中刪除給定的節點。 | 平均 O(N) ,最壞 O(N^2) 。 |
ziplistDeleteRange | 刪除壓縮列表在給定索引上的連續多個節點。 | 平均 O(N) ,最壞 O(N^2) 。 |
ziplistBlobLen | 返回壓縮列表目前佔用的記憶體位元組數。 | O(1) |
ziplistLen | 返回壓縮列表目前包含的節點數量。 | 節點數量小於 65535 時 O(1) , 大於 65535 時 O(N) 。 |
因為 ziplistPush 、 ziplistInsert 、 ziplistDelete 和 ziplistDeleteRange 四個函式都有可能會引發連鎖更新, 所以它們的最壞複雜度都是 O(N^2) 。
重點回顧
- 壓縮列表是一種為節約記憶體而開發的順序型資料結構。
- 壓縮列表被用作列表鍵和雜湊鍵的底層實現之一。
- 壓縮列表可以包含多個節點,每個節點可以儲存一個位元組陣列或者整數值。
- 新增新節點到壓縮列表, 或者從壓縮列表中刪除節點, 可能會引發連鎖更新操作, 但這種操作出現的機率並不高
相關文章
- Redis內部資料結構詳解(4)——ziplistRedis資料結構
- 蜻蜓點水說說Redis的ziplist的奧祕Redis
- Redis核心原理與實踐--列表實現原理之ziplistRedis
- Redis的概述Redis
- [Redis 概述] 什麼是 Redis?Redis
- 探索Redis設計與實現4:Redis內部資料結構詳解——ziplistRedis資料結構
- 跟我一起學Redis之Redis概述Redis
- Redis系列(九):資料結構Hash(ZipList、HashTable)原始碼解析和HSET、HGET命令Redis資料結構原始碼
- Redis主從複製流程概述Redis
- redis系列2知識點概述Redis
- 【Redis系列3】Redis列表物件之linkedlist(雙端列表)和ziplist(壓縮列表)及quicklick(快速列表)實現原理分析Redis物件UI
- Redis概述及基本資料結構Redis資料結構
- redis各資料型別應用概述Redis資料型別
- Redis常見面試題:ZSet底層資料結構,SDS、壓縮列表ZipList、跳錶SkipListRedis面試題資料結構
- 深入剖析Redis系列(四) - Redis資料結構與全域性命令概述Redis資料結構
- Redis主從複製斷點續傳的工作原理概述Redis斷點
- 好程式設計師Java培訓分享Redis快取使用場景概述程式設計師JavaRedis快取
- 概述
- Java概述Java
- Ocelot概述
- Servlet概述Servlet
- HBase概述
- hadoop概述Hadoop
- Promise 概述Promise
- mongodb 概述MongoDB
- EOSKeosd概述
- JVM 概述JVM
- DevOps概述dev
- OpenFeign概述
- ElasticSearch 概述Elasticsearch
- TCP 概述TCP
- JDBC概述JDBC
- Flume概述
- Android概述Android
- UML概述
- RXJS 概述JS
- (1)概述
- uoj概述