有序集合的編碼可以是?`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 所示的樣子。


`skiplist`?編碼的有序集合對象使用?`zset`?結構作為底層實現, 一個?`zset`?結構同時包含一個字典和一個跳躍表:
~~~
typedef struct zset {
zskiplist *zsl;
dict *dict;
} zset;
~~~
`zset`?結構中的?`zsl`?跳躍表按分值從小到大保存了所有集合元素, 每個跳躍表節點都保存了一個集合元素: 跳躍表節點的?`object`?屬性保存了元素的成員, 而跳躍表節點的?`score`?屬性則保存了元素的分值。 通過這個跳躍表, 程序可以對有序集合進行范圍型操作, 比如?ZRANK?、ZRANGE?等命令就是基于跳躍表 API 來實現的。
除此之外,?`zset`?結構中的?`dict`?字典為有序集合創建了一個從成員到分值的映射, 字典中的每個鍵值對都保存了一個集合元素: 字典的鍵保存了元素的成員, 而字典的值則保存了元素的分值。 通過這個字典, 程序可以用??復雜度查找給定成員的分值,?ZSCORE?命令就是根據這一特性實現的, 而很多其他有序集合命令都在實現的內部用到了這一特性。
有序集合每個元素的成員都是一個字符串對象, 而每個元素的分值都是一個?`double`?類型的浮點數。 值得一提的是, 雖然?`zset`?結構同時使用跳躍表和字典來保存有序集合元素, 但這兩種數據結構都會通過指針來共享相同元素的成員和分值, 所以同時使用跳躍表和字典來保存集合元素不會產生任何重復成員或者分值, 也不會因此而浪費額外的內存。
為什么有序集合需要同時使用跳躍表和字典來實現?
在理論上來說, 有序集合可以單獨使用字典或者跳躍表的其中一種數據結構來實現, 但無論單獨使用字典還是跳躍表, 在性能上對比起同時使用字典和跳躍表都會有所降低。
舉個例子, 如果我們只使用字典來實現有序集合, 那么雖然以??復雜度查找成員的分值這一特性會被保留, 但是, 因為字典以無序的方式來保存集合元素, 所以每次在執行范圍型操作 —— 比如?ZRANK?、?ZRANGE?等命令時, 程序都需要對字典保存的所有元素進行排序, 完成這種排序需要至少??時間復雜度, 以及額外的??內存空間 (因為要創建一個數組來保存排序后的元素)。
另一方面, 如果我們只使用跳躍表來實現有序集合, 那么跳躍表執行范圍型操作的所有優點都會被保留, 但因為沒有了字典, 所以根據成員查找分值這一操作的復雜度將從??上升為??。
因為以上原因, 為了讓有序集合的查找和范圍型操作都盡可能快地執行, Redis 選擇了同時使用字典和跳躍表兩種數據結構來實現有序集合。
舉個例子, 如果前面?`price`?鍵創建的不是?`ziplist`?編碼的有序集合對象, 而是?`skiplist`?編碼的有序集合對象, 那么這個有序集合對象將會是圖 8-16 所示的樣子, 而對象所使用的?`zset`?結構將會是圖 8-17 所示的樣子。


注意
為了展示方便, 圖 8-17 在字典和跳躍表中重復展示了各個元素的成員和分值, 但在實際中, 字典和跳躍表會共享元素的成員和分值, 所以并不會造成任何數據重復, 也不會因此而浪費任何內存。
## 編碼的轉換
當有序集合對象可以同時滿足以下兩個條件時, 對象使用?`ziplist`?編碼:
1. 有序集合保存的元素數量小于?`128`?個;
2. 有序集合保存的所有元素成員的長度都小于?`64`?字節;
不能滿足以上兩個條件的有序集合對象將使用?`skiplist`?編碼。
注意
以上兩個條件的上限值是可以修改的, 具體請看配置文件中關于?`zset-max-ziplist-entries`?選項和?`zset-max-ziplist-value`?選項的說明。
對于使用?`ziplist`?編碼的有序集合對象來說, 當使用?`ziplist`?編碼所需的兩個條件中的任意一個不能被滿足時, 程序就會執行編碼轉換操作, 將原本儲存在壓縮列表里面的所有集合元素轉移到?`zset`?結構里面, 并將對象的編碼從?`ziplist`?改為?`skiplist`?。
以下代碼展示了有序集合對象因為包含了過多元素而引發編碼轉換的情況:
~~~
# 對象包含了 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"
~~~
以下代碼則展示了有序集合對象因為元素的成員過長而引發編碼轉換的情況:
~~~
# 向有序集合添加一個成員只有三字節長的元素
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"
~~~
## 有序集合命令的實現
因為有序集合鍵的值為有序集合對象, 所以用于有序集合鍵的所有命令都是針對有序集合對象來構建的, 表 8-11 列出了其中一部分有序集合鍵命令, 以及這些命令在不同編碼的有序集合對象下的實現方法。
* * *
表 8-11 有序集合命令的實現方法
| 命令 | `ziplist`?編碼的實現方法 | `zset`?編碼的實現方法 |
| --- | --- | --- |
| ZADD | 調用?`ziplistInsert`?函數, 將成員和分值作為兩個節點分別插入到壓縮列表。 | 先調用?`zslInsert`?函數, 將新元素添加到跳躍表, 然后調用?`dictAdd`?函數, 將新元素關聯到字典。 |
| ZCARD | 調用?`ziplistLen`?函數, 獲得壓縮列表包含節點的數量, 將這個數量除以?`2`?得出集合元素的數量。 | 訪問跳躍表數據結構的?`length`?屬性, 直接返回集合元素的數量。 |
| ZCOUNT | 遍歷壓縮列表, 統計分值在給定范圍內的節點的數量。 | 遍歷跳躍表, 統計分值在給定范圍內的節點的數量。 |
| ZRANGE | 從表頭向表尾遍歷壓縮列表, 返回給定索引范圍內的所有元素。 | 從表頭向表尾遍歷跳躍表, 返回給定索引范圍內的所有元素。 |
| ZREVRANGE | 從表尾向表頭遍歷壓縮列表, 返回給定索引范圍內的所有元素。 | 從表尾向表頭遍歷跳躍表, 返回給定索引范圍內的所有元素。 |
| ZRANK | 從表頭向表尾遍歷壓縮列表, 查找給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之后, 途經節點的數量就是該成員所對應元素的排名。 | 從表頭向表尾遍歷跳躍表, 查找給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之后, 途經節點的數量就是該成員所對應元素的排名。 |
| ZREVRANK | 從表尾向表頭遍歷壓縮列表, 查找給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之后, 途經節點的數量就是該成員所對應元素的排名。 | 從表尾向表頭遍歷跳躍表, 查找給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之后, 途經節點的數量就是該成員所對應元素的排名。 |
| ZREM | 遍歷壓縮列表, 刪除所有包含給定成員的節點, 以及被刪除成員節點旁邊的分值節點。 | 遍歷跳躍表, 刪除所有包含了給定成員的跳躍表節點。 并在字典中解除被刪除元素的成員和分值的關聯。 |
| ZSCORE | 遍歷壓縮列表, 查找包含了給定成員的節點, 然后取出成員節點旁邊的分值節點保存的元素分值。 | 直接從字典中取出給定成員的分值。 |
- 介紹
- 前言
- 致謝
- 簡介
- 第一部分:數據結構與對象
- 簡單動態字符串
- SDS 的定義
- SDS 與 C 字符串的區別
- SDS API
- 重點回顧
- 參考資料
- 鏈表
- 鏈表和鏈表節點的實現
- 鏈表和鏈表節點的 API
- 重點回顧
- 字典
- 字典的實現
- 哈希算法
- 解決鍵沖突
- rehash
- 漸進式 rehash
- 字典 API
- 重點回顧
- 跳躍表
- 跳躍表的實現
- 跳躍表 API
- 重點回顧
- 整數集合
- 整數集合的實現
- 升級
- 升級的好處
- 降級
- 整數集合 API
- 重點回顧
- 壓縮列表
- 壓縮列表的構成
- 壓縮列表節點的構成
- 連鎖更新
- 壓縮列表 API
- 重點回顧
- 對象
- 對象的類型與編碼
- 字符串對象
- 列表對象
- 哈希對象
- 集合對象
- 有序集合對象
- 類型檢查與命令多態
- 內存回收
- 對象共享
- 對象的空轉時長
- 重點回顧
- 第二部分:單機數據庫的實現
- 數據庫
- 數據庫鍵空間
- 重點回顧
- RDB 持久化
- RDB 文件結構
- 重點回顧
- AOF 持久化
- AOF 持久化的實現
- 重點回顧
- 事件
- 文件事件
- 重點回顧
- 參考資料
- 客戶端
- 客戶端屬性
- 重點回顧
- 服務器
- 命令請求的執行過程
- 重點回顧
- 第三部分:多機數據庫的實現
- 復制
- 舊版復制功能的實現
- 重點回顧
- Sentinel
- 啟動并初始化 Sentinel
- 重點回顧
- 參考資料
- 集群
- 節點
- 重點回顧
- 第四部分:獨立功能的實現
- 發布與訂閱
- 頻道的訂閱與退訂
- 重點回顧
- 參考資料
- 事務
- 事務的實現
- 重點回顧
- Lua 腳本
- 創建并修改 Lua 環境
- 重點回顧
- 排序
- SORT <key> 命令的實現
- 重點回顧
- 二進制位數組
- GETBIT 命令的實現
- 重點回顧
- 慢查詢日志
- 慢查詢記錄的保存
- 慢查詢日志的閱覽和刪除
- 添加新日志
- 重點回顧
- 監視器
- 成為監視器
- 向監視器發送命令信息
- 重點回顧
- 源碼、相關資源和勘誤