Redis底層探秘(五):Redis對象
前面幾篇文章,我們一起學習了redis用到的所有主要資料結構,比如簡單動态字元串(sds)、雙端連結清單、字典、壓縮清單、整數集合等等。
redis并沒有直接使用這些資料結構來實作鍵值對資料庫,而是基于這些資料結建構立了一個對象系統,這個系統包含字元串對象、清單對象、哈希對象、集合對象和有序集合對象這五種類型的對象,每種對象都用到了至少一種我們前面所介紹的資料結構。
通過這五種這五種不同類型的對象,redis可以在執行指令之前,根據對象的類型來判斷一個對象是否可以執行給定的指令。使用對象的另一個好處是,我們可以針對不同的使用場景,為對象設定多種不同的資料結構實作,進而優化對象在不同場景下的使用效率。
除此之外,redis的對象系統還實作了基于引用計數技術的記憶體回收機制,當程式不再使用某個對象的時候,這個對象所占用的記憶體就會被自動釋放;另外,redis還通過引用計數技術實作了對象共享機制,這一機制可以在适當條件下,通過讓多個資料庫鍵共享同一個對象來節約記憶體。
最後,redis對象帶有通路時間記錄資訊,該資訊可以用于計算資料庫鍵的空轉時長,在伺服器啟用了maxmenory功能的情況下,空轉時長較大的那些鍵可能會優先被伺服器删除。
接下來我們将逐一學習以上提到的redis對象和特性。
對象的類型與編碼
redis使用對象來表示資料庫中的鍵和值,每次當我們在redis 的資料庫中新建立一個鍵值對時,我們至少會建立兩個對象,一個對象用作鍵值對的鍵,另一個對象用于鍵值對的值。
reids中的每個對象都由一個redisObject結構表示,該結構中和儲存資料有關的三個屬性如下
typedef struct redisObject{
//類型
unsigned type:4;
//編碼
unsigned encoding:4;
//指向底層實作資料結構的指針
void *ptr;
…….
}robj
類型
對象的type屬性記錄了對象的類型,這個屬性的值是下表中其中的一個。
對于redis資料庫儲存的鍵值對鍵值對俺來說,鍵總是一個字元串對象,而值則可以是字元串對象、清單對象、哈希對象、集合對象或者有序集合對象其中一種。
是以我們執行type指令時,指令傳回的結果為資料庫鍵對應的值對象的類型,而不是鍵對象的類型。
set msg “hello”
type msg//string
不同值類型所對應的的輸出如下
編碼和底層實作
對象ptr指針指向對象的底層實作資料結構,而這些資料結構由對象的encoding屬性決定。
encoding屬性記錄了對象所使用的編碼,也即是說這個對象使用了什麼資料結構作為對象的底層實作,這個屬性可以是下面列出的常量的其中之一
每種類型的對象都至少使用了兩種不同的編碼,每種類型的對象可以使用的編碼如下
object encoding 指令可以檢視資料庫值對象的編碼
object encoding msg//“embstr”
下圖列出了不同編碼對象對應的object encoding輸出
通過encoding屬性來設定對象所用的編碼,而不是為特定類型的對象對象關聯一種固定的編碼,極大地提升了redis的靈活性和效率,因為redis可以根據不同的使用場景來為一個對象設定不同的編碼,進而優化對象在某一場景下的效率。
舉個例子,清單對象包含的元素比較少的時候,redis使用壓縮清單作為底層實作:
1 因為壓縮清單比雙端連結清單更節約記憶體,并且在元素數量較少時,在記憶體中以連續塊方式儲存的壓縮清單比起雙端連結清單可以更快被趙茹到緩存中。
2 随着清單對象包含的元素越來越多,使用壓縮清單來儲存元素的優勢逐漸消失時,對象就會将底層實作從壓縮清單轉向功能更強、更适合儲存大量元素的雙端連結清單。
其他類型的對象也會通過使用多種不同的編碼來進行類型的優化。
接下來,我們分别學習redis五種不同類型的對象,他們使用的編碼方式,轉換條件,以及同一個指令在多種不同編碼上的實作方法。
字元串對象
字元串對象的編碼可以使int,raw或者embstr。
如果一個字元串對象儲存的是整數值,并且這個整數值可以用long類型來表示,那麼字元串對象會将整數值儲存在字元串對象結構的ptr屬性裡面(将void *轉換成long),并将字元串對象的編碼設定為int。
舉個例子,執行以下指令
set number 10086
object encoding number //”int”
結構如下圖
如果字元串對象儲存的是一個字元串值,并且這個字元串值的長度大于32位元組,那麼字元串對象将使用一個簡單動态字元串(sds)來儲存這個字元串值,并将對象的編碼設定為raw。
如果字元串對象儲存的是一個字元串值,并且這個字元串值的長度小于等于32位元組,那麼字元串對象将使用embstr編碼的方式來儲存這個字元串值。
embstr編碼是專門用于儲存短字元串的一種優化編碼方式,這種編碼和raw編碼一樣,都使用redisObject結構和sdshdr結構來表示字元串對象,但raw編碼會調用兩次記憶體配置設定函數來分别建立redisObject結構和sdshdr結構,而embstr編碼則通過調用一次記憶體配置設定函數來配置設定一塊連續的空間,空間中一次包含redisObject和sdshdr連個結構。如下圖
embstr編碼的字元串對象在執行指令時,産生的效果和raw編碼的字元串對象執行指令時産生的效果是相同的,但使用embstr編碼的字元串對象來儲存短字元串值有以下好處:
1 embstr編碼将建立字元串對象所需的記憶體配置設定次數從raw編碼的兩次降為一次。
2 釋放embstr編碼的字元串對象隻需要調用一次記憶體釋放函數,而釋放raw編碼的字元串對象需要調用兩次記憶體釋放函數。
3 因為embstr編碼的字元串對象的所有資料都儲存在一塊連續的記憶體裡面,是以這種編碼的字元串對象比起raw編碼的字元串對象能夠更好地利用緩存帶來的優勢。
最後要說的是,可以用long double類型表示的浮點數在redis中也是作為字元串值來儲存的。如果我們要儲存一個浮點數到字元串對象裡面,那麼程式會先将這個浮點數轉換成字元串值,然後再儲存轉換所得的字元串值。
int編碼的字元串對象和embstr編碼的字元串對象在條件滿足的情況下,會被轉換為raw編碼的字元串對象。
字元串指令的實作
清單對象
清單對象的編碼可以使ziplist或者linkedlist。
ziplist編碼的清單對象使用壓縮清單作為底層實作,每個壓縮清單節點(entry)儲存了一個清單元素。下圖就是ziplist編碼的清單對象,紅框内為存儲的資料。
另一方面,linkedlist編碼的清單對象使用雙端連結清單作為底層實作,每個雙端連結清單節點都儲存了一個字元串對象,而每個字元串對象都儲存了一個清單元素,如下圖
注意,linkedlist編碼的清單對象在底層的雙端連結清單結構中包含了多個字元串對象,這種這種嵌套字元串對象的行為在稍後介紹的哈希對象、集合對象和有序集合對象中都會出現,字元串對象是redis五種類型的對象中唯一一種會被其他四中類型對象嵌套對象。
為了台灣字元串對象的表示,我們使用StringObject字樣來代表字元串對象,完整格式如下
編碼轉換
當清單對象可以同時滿足以下兩個條件時,清單對象使用ziplist編碼:
1 清單對象儲存的所有字元串元素的長度都小于64位元組
2 清單對象儲存的元素數量小于512個;
(以上兩個條件的上限值可以修改)
清單指令的實作
哈希對象
哈希對象的編碼可以是ziplist或者hashtable。
ziplist編碼的哈希對象使用壓縮清單作為底部實作,每當有新的鍵值對要加入到哈希對象時,程式會先儲存了鍵的壓縮清單節點推入到壓縮清單表尾,然後再将儲存了值的壓縮清單節點推入到壓縮清單表尾,是以:
1 儲存了同一鍵值對的兩個節點總是緊挨在一起,儲存鍵的節點在前,儲存值的節點在後
2 先添加到哈希對象中的鍵值對會被放在壓縮清單的表頭方向,而後來添加到哈希對象中的鍵值對會被放在壓縮清單的表尾方向。
舉個例子,執行如下指令
hset profile name “Tom”
hset profile age 25
hset profile career “Programmer”
那麼,他的編碼回事ziplist,對應的哈希對象如下圖
另一方面,hashtable編碼的哈希對象使用字典作為底層實作,哈希對象中的每個鍵值對都使用一個字典鍵值對來儲存
1 字典的每個鍵都是一個字元串對象,對象中儲存了鍵值對的鍵
2 字典的每個值都是一個字元串對象,對象中儲存了鍵值對的值
上例中,對應的hashtable編碼的哈希對象如下圖
當哈希對象可以同時滿足一下兩個條件時,哈希對象使用ziplist編碼
1 哈希對象儲存的所有鍵值對的鍵和值字元串長度都小于64位元組。
2 哈希對象儲存的鍵值對數量小于512個
不能滿足這兩個條件的哈希對象需要使用hashtable編碼(這兩個條件的上限值可以在redis配置中修改。)
哈希指令的實作
因為哈希鍵的值為哈希對象,是以用于哈希鍵的所有指令都是針對哈希對象來建構的,下表列出一部分哈希鍵指令,以及這些指令在不同編碼對象下的實作方法。
這裡就單獨說下Hget方法,雖然ziplist編碼時,ziplistFind方法複雜度為O(N),但是鍵值對總數較少,不會超過256,執行速度也是很快的。鍵值對多的時候,hashtable的實作,複雜度為O(1),執行起來的速度也是很快。
集合對象
集合對象的編碼可以是intset或者hashtable。
intset編碼的集合對象使用證書集合作為底層實作,集合對象包含的所有元素都被儲存在整數集合裡面。
另一方面,hashtable編碼的集合對象使用字典作為底層實作,字典的每個鍵都是一個字元串對象,每個字元串對象包含了一個集合元素,而字典的值則全部被設定為null。
當集合對象可以同時滿足一下兩個條件時,對象使用intset編碼:
1 集合對象儲存的所有元素都是整數值
2 集合對象儲存的元素數量不超過512個
不能滿足這兩個條件的集合對象使用hashtable編碼。
集合指令的實作
有序集合對象
有序集合的編碼可以是ziplist或者skiplist。
ziplist編碼的壓縮清單對象使用壓縮清單作為底層實作,每個集合元素使用兩個金愛在一起的壓縮清單節點儲存,第一個節點儲存元素的成員,而第二個元素則儲存元素的分值。
壓縮清單内的集合元素按分值從小到大金星排序,分值較小的元素被防止在靠近表頭的方向,而分值較大的元素責備防止在靠近表尾的方向,如下圖。
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結構同時使用跳躍表和字典來儲存有序集合元素,但這兩種資料結構都會通過指針來共享相同的成員和分值,是以同時使用跳躍表和字典來儲存集合元素不會産生任何重複成員或分值,也不會是以而浪費額外的記憶體。
編碼的轉換
黨有序集合對象可以同時滿足以下兩個條件時,對象使用ziplist編碼:
1 有序集合同時保持的元素數量小于128個
2 有序集合儲存的所有元素成員的長度都小于64位元組