對象
Redis 並沒有直接使用之前的數據結構來實現鍵值對數據庫, 而是基於這些數據結構創建了一個對象系統, 這個系統包含字符串對象、列表對象、哈希對象、集合對象和有序集合對象這五種類型的對象, 每種對象都用到了至少一種我們前面所介紹的數據結構。
除此之外, Redis 的對象系統還實現了基於引用計數技術的內存回收機制: 當程序不再使用某個對象的時候, 這個對象所佔用的內存就會被自動釋放; 另外, Redis 還通過引用計數技術實現了對象共享機制, 這一機制可以在適當的條件下, 通過讓多個數據庫鍵共享同一個對象來節約內存。
對象的類型與編碼
Redis 中的每個對象都由一個 redisObject 結構表示
typedef struct redisObject {
// 類型
unsigned type:4;
// 編碼
unsigned encoding:4;
// 指向底層實現數據結構的指針
void *ptr;
// ...
} robj;
類型
對於 Redis 數據庫保存的鍵值對來說, 鍵總是一個字符串對象, 而值則可以是字符串對象、列表對象、哈希對象、集合對象或者有序集合對象的其中一種。
類型常量 | 對象的名稱 |
---|---|
REDIS_STRING | 字符串對象 |
REDIS_LIST | 列表對象 |
REDIS_HASH | 哈希對象 |
REDIS_SET | 集合對象 |
REDIS_ZSET | 有序集合對象 |
編碼和底層實現
對象的 ptr 指針指向對象的底層實現數據結構,encoding 屬性記錄了對象所使用的編碼。
編碼常量 | 編碼所對應的底層數據結構 |
---|---|
REDIS_ENCODING_INT | long 類型的整數 |
REDIS_ENCODING_EMBSTR | embstr 編碼的簡單動態字符串 |
REDIS_ENCODING_RAW | 簡單動態字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 雙端鏈表 |
REDIS_ENCODING_ZIPLIST | 壓縮列表 |
REDIS_ENCODING_INTSET | 整數集合 |
REDIS_ENCODING_SKIPLIST | 跳躍表和字典 |
字符串對象
字符串對象的編碼可以是 int 、 raw 或者 embstr 。
如果一個字符串對象保存的是整數值, 並且這個整數值可以用 long 類型來表示, 那麼字符串對象會將整數值保存在字符串對象結構的 ptr 屬性裏面(將 void* 轉換成 long ), 並將字符串對象的編碼設置爲 int 。
如果字符串對象保存的是一個字符串值, 並且這個字符串值的長度大於 39 字節, 那麼字符串對象將使用一個簡單動態字符串(SDS)來保存這個字符串值, 並將對象的編碼設置爲 raw 。
如果字符串對象保存的是一個字符串值, 並且這個字符串值的長度小於等於 39 字節, 那麼字符串對象將使用 embstr 編碼的方式來保存這個字符串值。
embstr 編碼的字符串對象在執行命令時, 產生的效果和 raw 編碼的字符串對象執行命令時產生的效果是相同的, 但使用 embstr 編碼的字符串對象來保存短字符串值有以下好處:
- embstr 編碼將創建字符串對象所需的內存分配次數從 raw 編碼的兩次降低爲一次。
- 釋放 embstr 編碼的字符串對象只需要調用一次內存釋放函數, 而釋放 raw 編碼的字符串對象需要調用兩次內存釋放函數。
- 因爲 embstr 編碼的字符串對象的所有數據都保存在一塊連續的內存裏面, 所以這種編碼的字符串對象比起 raw 編碼的字符串對象能夠更好地利用緩存帶來的優勢。
可以用 long double 類型表示的浮點數在 Redis 中也是作爲字符串值來保存的。 如果我們要保存一個浮點數到字符串對象裏面, 那麼程序會先將這個浮點數轉換成字符串值, 然後再保存起轉換所得的字符串值。使用時會將字符串值轉換回浮點數值, 執行某些操作, 然後再將執行操作所得的浮點數值轉換回字符串值, 並繼續保存在字符串對象裏面。
編碼轉換
對於 int 編碼的字符串對象來說, 如果我們向對象執行了一些命令, 使得這個對象保存的不再是整數值, 而是一個字符串值, 那麼字符串對象的編碼將從 int 變爲 raw 。
因爲 Redis 沒有爲 embstr 編碼的字符串對象編寫任何相應的修改程序 (只有 int 編碼的字符串對象和 raw 編碼的字符串對象有這些程序), 所以 embstr 編碼的字符串對象實際上是隻讀的: 當我們對 embstr 編碼的字符串對象執行任何修改命令時, 程序會先將對象的編碼從 embstr 轉換成 raw , 然後再執行修改命令; 因爲這個原因, embstr 編碼的字符串對象在執行修改命令之後, 總會變成一個 raw 編碼的字符串對象。
列表對象
列表對象的編碼可以是 ziplist 或者 linkedlist。
linkedlist 編碼的列表對象在底層的雙端鏈表結構中包含了多個字符串對象, 這種嵌套字符串對象的行爲在稍後介紹的哈希對象、集合對象和有序集合對象中都會出現, 字符串對象是 Redis 五種類型的對象中唯一一種會被其他四種類型對象嵌套的對象。
編碼轉換
當列表對象可以同時滿足以下兩個條件時, 列表對象使用 ziplist 編碼:
- 列表對象保存的所有字符串元素的長度都小於 64 字節;
- 列表對象保存的元素數量小於 512 個;
不能滿足這兩個條件的列表對象需要使用 linkedlist 編碼。
以上兩個條件的上限值是可以修改的, 具體請看配置文件中關於 list-max-ziplist-value 選項和 list-max-ziplist-entries 選項的說明。
哈希對象
哈希對象的編碼可以是 ziplist 或者 hashtable 。
ziplist 編碼的哈希對象使用壓縮列表作爲底層實現, 每當有新的鍵值對要加入到哈希對象時, 程序會先將保存了鍵的壓縮列表節點推入到壓縮列表表尾, 然後再將保存了值的壓縮列表節點推入到壓縮列表表尾。
hashtable 編碼的哈希對象使用字典作爲底層實現, 哈希對象中的每個鍵值對都使用一個字典鍵值對來保存:
編碼轉換
當哈希對象可以同時滿足以下兩個條件時, 哈希對象使用 ziplist 編碼:
- 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小於 64 字節;
- 哈希對象保存的鍵值對數量小於 512 個;
不能滿足這兩個條件的哈希對象需要使用 hashtable 編碼。
集合對象
集合對象的編碼可以是 intset 或者 hashtable 。
hashtable 編碼的集合對象使用字典作爲底層實現, 字典的每個鍵都是一個字符串對象, 每個字符串對象包含了一個集合元素, 而字典的值則全部被設置爲 NULL 。
編碼轉換
當集合對象可以同時滿足以下兩個條件時, 對象使用 intset 編碼:
- 集合對象保存的所有元素都是整數值;
- 集合對象保存的元素數量不超過 512 個;
有序集合對象
有序集合的編碼可以是 ziplist 或者 skiplist 。
ziplist 編碼的有序集合對象使用壓縮列表作爲底層實現, 每個集合元素使用兩個緊挨在一起的壓縮列表節點來保存, 第一個節點保存元素的成員(member), 而第二個元素則保存元素的分值(score)。
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 結構同時使用跳躍表和字典來保存有序集合元素, 但這兩種數據結構都會通過指針來共享相同元素的成員和分值, 所以同時使用跳躍表和字典來保存集合元素不會產生任何重複成員或者分值, 也不會因此而浪費額外的內存。
爲什麼有序集合需要同時使用跳躍表和字典來實現?
爲了利用字典以 O(1) 複雜度查找成員,利用跳錶執行範圍型操作。
編碼轉換
當有序集合對象可以同時滿足以下兩個條件時, 對象使用 ziplist 編碼:
- 有序集合保存的元素數量小於 128 個;
- 有序集合保存的所有元素成員的長度都小於 64 字節;
參考資料