標籤:typedef 轉換 提升 hello 訪問時間 等於 技術分享 編碼方式 表結構
前面幾篇文章,我們一起學習了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位元組
有序集合命令的實現
Redis底層探秘(五):Redis對象