《閒扯Redis十一》Redis 有序集合物件底層實現

jstarseven發表於2020-09-09

一、前言

Redis 提供了5種資料型別:String(字串)、Hash(雜湊)、List(列表)、Set(集合)、Zset(有序集合),理解每種資料型別的特點對於redis的開發和運維非常重要。

原文解析

Redis五種資料型別

備註: 本節中涉及到的跳躍表實現,已經在上節《閒扯Redis十》Redis 跳躍表的結構實現一文中詳情分析過,本文中將直接引用,不再贅述。

二、命令實現

 因為有序集合鍵的值為有序集合物件,所以用於有序集合鍵的所有命令都是針對有序集合物件來構建的。

命令 ziplist 編碼的實現方法 zset 編碼的實現方法
ZADD 呼叫 ziplistInsert 函式, 將成員和分值作為兩個節點分別插入到壓縮列表。 先呼叫 zslInsert 函式, 將新元素新增到跳躍表, 然後呼叫 dictAdd 函式, 將新元素關聯到字典。
ZCARD 呼叫 ziplistLen 函式, 獲得壓縮列表包含節點的數量, 將這個數量除以 2 得出集合元素的數量。 訪問跳躍表資料結構的 length 屬性, 直接返回集合元素的數量。
ZCOUNT 遍歷壓縮列表, 統計分值在給定範圍內的節點的數量。 遍歷跳躍表, 統計分值在給定範圍內的節點的數量。
ZRANGE 從表頭向表尾遍歷壓縮列表, 返回給定索引範圍內的所有元素。 從表頭向表尾遍歷跳躍表, 返回給定索引範圍內的所有元素。
ZREVRANGE 從表尾向表頭遍歷壓縮列表, 返回給定索引範圍內的所有元素。 從表尾向表頭遍歷跳躍表, 返回給定索引範圍內的所有元素。
ZRANK 從表頭向表尾遍歷壓縮列表, 查詢給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之後, 途經節點的數量就是該成員所對應元素的排名。 從表頭向表尾遍歷跳躍表, 查詢給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之後, 途經節點的數量就是該成員所對應元素的排名。
ZREVRANK 從表尾向表頭遍歷壓縮列表, 查詢給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之後, 途經節點的數量就是該成員所對應元素的排名。 從表尾向表頭遍歷跳躍表, 查詢給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之後, 途經節點的數量就是該成員所對應元素的排名。
ZREM 遍歷壓縮列表, 刪除所有包含給定成員的節點, 以及被刪除成員節點旁邊的分值節點。 遍歷跳躍表, 刪除所有包含了給定成員的跳躍表節點。 並在字典中解除被刪除元素的成員和分值的關聯。
ZSCORE 遍歷壓縮列表, 查詢包含了給定成員的節點, 然後取出成員節點旁邊的分值節點儲存的元素分值。 直接從字典中取出給定成員的分值。

三、結構解析

 由前文和上圖可知,有序集合的編碼可以是 ziplist 或者 skiplist 。ziplist 編碼的有序集合物件使用壓縮列表作為底層實現, 每個集合元素使用兩個緊挨在一起的壓縮列表節點來儲存, 第一個節點儲存元素的成員(member), 而第二個元素則儲存元素的分值(score)。

壓縮列表方式

壓縮列表內的集合元素按分值從小到大進行排序, 分值較小的元素被放置在靠近表頭的方向, 而分值較大的元素則被放置在靠近表尾的方向。

例如,如果我們執行以下 ZADD 命令, 那麼伺服器將建立一個有序集合物件作為 price 鍵的值:

redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3

如果 price 鍵的值物件使用的是 ziplist 編碼, 那麼這個值物件將會是圖 8-14 所示,, 而物件所使用的壓縮列表則會是 8-15 所示。

Redis五種資料型別
Redis五種資料型別

跳躍表和字典方式

 skiplist 編碼的有序集合物件使用 zset 結構作為底層實現, 一個 zset 結構同時包含一個字典和一個跳躍表:

typedef struct zset {

    zskiplist *zsl;

    dict *dict;

} zset;

zset 結構中的 zsl 跳躍表按分值從小到大儲存了所有集合元素, 每個跳躍表節點都儲存了一個集合元素: 跳躍表節點的 object 屬性儲存了元素的成員, 而跳躍表節點的 score 屬性則儲存了元素的分值。 通過這個跳躍表, 程式可以對有序集合進行範圍型操作, 比如 ZRANK 、ZRANGE 等命令就是基於跳躍表 API 來實現的。

除此之外, zset 結構中的 dict 字典為有序集合建立了一個從成員到分值的對映, 字典中的每個鍵值對都儲存了一個集合元素: 字典的鍵儲存了元素的成員, 而字典的值則儲存了元素的分值。 通過這個字典, 程式可以用 O(1) 複雜度查詢給定成員的分值, ZSCORE 命令就是根據這一特性實現的, 而很多其他有序集合命令都在實現的內部用到了這一特性。

有序集合每個元素的成員都是一個字串物件, 而每個元素的分值都是一個 double 型別的浮點數。 值得一提的是, 雖然 zset 結構同時使用跳躍表和字典來儲存有序集合元素, 但這兩種資料結構都會通過指標來共享相同元素的成員和分值, 所以同時使用跳躍表和字典來儲存集合元素不會產生任何重複成員或者分值, 也不會因此而浪費額外的記憶體

skiplist 編碼的有序集合物件, 那麼這個有序集合物件將會是圖 8-16 所示, 而物件所使用的 zset 結構將會是圖 8-17 所示。

Redis五種資料型別
Redis五種資料型別

注意:
  為了展示方便, 圖 8-17 在字典和跳躍表中重複展示了各個元素的成員和分值, 但在實際中, 字典和跳躍表會共享元素的成員和分值, 所以並不會造成任何資料重複, 也不會因此而浪費任何記憶體。

四、編碼轉換

當有序集合物件可以同時滿足以下兩個條件時, 物件使用 ziplist 編碼:

1.有序集合儲存的元素數量小於 128 個;
2.有序集合儲存的所有元素成員的長度都小於 64 位元組;

不能滿足以上兩個條件的有序集合物件將使用 skiplist 編碼。

注意:
  以上兩個條件的上限值是可以修改的, 具體請看配置檔案中關於 zset-max-ziplist-entries 選項和 zset-max-ziplist-value 選項的說明。對於使用 ziplist 編碼的有序集合物件來說, 當使用 ziplist 編碼所需的兩個條件中的任意一個不能被滿足時, 程式就會執行編碼轉換操作, 將原本儲存在壓縮列表裡面的所有集合元素轉移到 zset 結構裡面, 並將物件的編碼從 ziplist 改為 skiplist 。

1.以下情況展示了有序集合物件因為包含了過多元素而引發編碼轉換:
# 物件包含了 128 個元素
redis> EVAL "for i=1, 128 do redis.call('ZADD', KEYS[1], i, i) end" 1 numbers
(nil)

redis> ZCARD numbers
(integer) 128

redis> OBJECT ENCODING numbers
"ziplist"

# 再新增一個新元素
redis> ZADD numbers 3.14 pi
(integer) 1

# 物件包含的元素數量變為 129 個
redis> ZCARD numbers
(integer) 129

# 編碼已改變
redis> OBJECT ENCODING numbers
"skiplist"

2.以下情況展示了有序集合物件因為元素的成員過長而引發編碼轉換:
# 向有序集合新增一個成員只有三位元組長的元素
redis> ZADD blah 1.0 www
(integer) 1

redis> OBJECT ENCODING blah
"ziplist"

# 向有序集合新增一個成員為 66 位元組長的元素
redis> ZADD blah 2.0 oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
(integer) 1

# 編碼已改變
redis> OBJECT ENCODING blah
"skiplist"

五、要點總結

1)有序集合的編碼可以是 ziplist 或者 skiplist。

2)一個 zset 結構同時包含一個字典和一個跳躍表。

3)zset 結構跳躍表和字典通過指標來共享相同元素的成員和分值。

4)有序集合物件 ziplist 或者 skiplist編碼,符合條件時可發生編碼轉換。

over

Redis五種資料型別

相關文章