Redis設計與實現——第一部分 數據結構與對象 第8章 對象

Reids並沒有直接使用這些數據結構來實現鍵值對數據庫,而是基於這些數據結構創建了一個對象系統,這個系統包含字符串對象、列表對象、哈希對象、集合對象和有序集合對象這五種類型的對象,每種對象至少一種數據結構。
Redis根據對象的類型來判斷對象是否可以執行給定的命令。使用對象可以針對不同的場景爲對象設置多種不同的數據結構實現,從而優化對象在不同場景下的使用效率。
Redis的對象系統還實現了基於引用計數的內存回收機制,當程序不再使用某個對象的時候,這個對象所佔用的內存就會被自動釋放,另外redis還通過引用計數技術實現了對象共享機制。另外對象還帶有訪問時間記錄信息,該信息在服務器啓用了maxmemory功能的情況系啊,空轉時長較大的那些鍵可能會優化被服務器刪除。

對象的類型與編碼
Redis使用對象來表示數據庫中的鍵和值,每次當我們在redis的數據庫中新創建一個鍵值對時,我們至少會創建兩個對象,一個對象用作鍵值對的鍵(鍵對象),另一個對象用作鍵值對的值(值對象)。
Redis中的每個對象都由一個redisObject結構表示,該結構中和保存數據有關的三個屬性分別是type屬性,encoding屬性和ptr屬性:
typedef struct redisObject {
//類型
unsigned type:4;
//編碼
unsigned encoding:4;
//指向底層實現的數據結構的指針
void *prt;
// …
} robj;

類型
鍵的類型總是一個字符串對象
值的類型可以表8-1列出的常量的其中一個。
在這裏插入圖片描述

TYPE命令返回的也是值對象的類型,而不是鍵對象的類型。
在這裏插入圖片描述

編碼和底層實現
對象的ptr指針指向對象的底層實現數據結構,而這些數據結構由對象的encoding屬性決定。encoding屬性記錄了對象所使用的編碼,也即是說這個對象使用了什麼數據結構作爲對象的底層實現,這個屬性的值可以是表8-3列出的常量中的一個。
在這裏插入圖片描述

每種類型的對象都至少使用了兩種不同的編碼來實現,表8-4列出了每種類型的對象可以使用的編碼。
在這裏插入圖片描述

通過encoding屬性來設定對象所使用的編碼,而不是爲特定類型的對象關聯一種固定的編碼,極大地提升了Redis的靈活性和效率,因爲Redis可以根據不同的使用場景來爲一個對象設置不同的編碼,從
而優化對象在某一個場景下的效率。

舉個例子,列表對象包含的元素比較少時,Redis使用壓縮列表作爲對象的底層實現:
1.因爲壓縮列表比雙端鏈表節約內存,並且在元素數量較少時,在內存中以連續塊的方式保存的壓縮列表比起雙端鏈表可以更快載入內存
2.隨着列表對象包含的元素越來越多,使用壓縮列表來保存元素的優勢逐漸消失時,對象就會將底層實現從壓縮列表轉向功能更強、也更適合保存大量元素的雙端鏈表上面。

字符串對象
值 編碼
可以用long類型保存的整數 int
可以用long double類型保存的浮點數 embstr或raw
字符串值,或者因爲長度太大而沒辦法用long類型表示的整數,又或者因爲長度太大而沒辦法用long double類型表示的浮點數 embstr或者raw
字符串值,並且長度大於32字節 raw
字符串值,長度小於等於32字節 embstr

embstr編碼是專門用於保存短字符串的一種優化編碼方式,這種編碼和raw編碼一樣,都使用redisObject結構和sdshdr結構來表示字符串對象,但使用embstr編碼的字符串對象來保存短字符串有以下好處:
1.embstr編碼將創建字符串對象所需的內存分配次數從raw的兩次降低到一次。
2.釋放embstr編碼的字符串對象只需要調用一次內存釋放函數,而raw需要兩次
3.因爲embstr編碼的字符串對象的所有數據結構都保存在一塊連續的內存裏面,所以這種編碼的字符串對象比起raw編碼的字符串對象能夠更好的利用緩存帶來的優勢。

編碼的轉換
int編碼和embstr編碼的字符串對象在條件滿足的情況下,會被轉換爲raw編碼的字符串對象。
embstr編碼是隻讀的,int編碼和raw可以修改,所以embstr在修改命令執行之後就變成了一個raw編碼的字符串對象。

字符串命令的實現
在這裏插入圖片描述

列表對象
列表對象的編碼可以是ziplist或者linkedlist。
在這裏插入圖片描述
在這裏插入圖片描述

編碼轉換
當列表對象可以同時滿足以下兩個條件時,列表對象使用ziplist編碼:
1.列表對象保存的所有字符串元素的長度都小於64字節,
2.列表對象保存的元素數量小於512個
其它使用linkedlist編碼

注意
以上兩個條件的上限值是可以修改的,具體請看配置文件中關於list-max-ziplist-value選項和list-max-ziplist-entries選項的說明。

列表命令的實現
在這裏插入圖片描述

哈希對象
哈希對象的編碼可以是ziplist或者hashtable。
ziplist編碼的哈希對象使用壓縮列表作爲底層實現,每當有新的鍵值對要加入到哈希對象時,程序會將保存了鍵的壓縮列表節點推入到壓縮列表表尾,然後再將保存了值的壓縮列表節點推入到壓縮列表表尾。因此:1.保存了同一鍵值對的兩個節點總是緊挨在一起,保存鍵的節點在前,保存值的節點在後;2.先添加到哈希對象中的鍵值對會放在壓縮列表的表頭方向,而後來添加到哈希對象中的鍵值對會被放在壓縮列表的表尾方向。

在這裏插入圖片描述
在這裏插入圖片描述

hashtable編碼的哈希對象使用字典作爲底層實現,哈希對象中的每個鍵值對都使用一個字典鍵值對來保存。
另一方面,hashtable編碼的哈希對象使用字典作爲底層實現,哈希對象中的每個鍵值對都使用一個字典鍵值對來保存:1.字典的每個鍵都是一個字符串對象,對象中保存了鍵值對的鍵;2.字典的每個值都是一個字符串對象,對象中保存了鍵值對的值。
在這裏插入圖片描述

編碼轉換:
當列表對象可以同時滿足以下兩個條件時,列表對象使用ziplist編碼:
1.列表對象保存的所有字符串元素的長度都小於64字節,
2.列表對象保存的元素數量小於512個
其它使用hashtable編碼
注意
以上兩個條件的上限值是可以修改的,具體請看配置文件中關於hash-max-ziplist-value選項和hash-max-ziplist-entries選項的說明。

哈希命令的實現
在這裏插入圖片描述

集合對象
集合對象的編碼可以是intset或者hashtable。
intset編碼的集合對象使用整數集合作爲底層實現,集合對象包含的所有元素都被保存在整數集合裏面。另一方面,hashtable編碼的集合對象使用字典作爲底層實現,字典的每一個鍵都是一個字符串對象,每個字符串對象包含了一個集合元素,而字典的值則全部被設置爲NULL。
在這裏插入圖片描述

編碼轉換
當列表對象可以同時滿足以下兩個條件時,列表對象使用intset編碼:
1.列表對象保存的所有元素的都是整數值,
2.列表對象保存的元素數量小於512個
其它使用hashtable編碼
注意
以上兩個條件的上限值是可以修改的,具體請看配置文件中關於set-max-inset-entries選項的說明。
在這裏插入圖片描述
在這裏插入圖片描述

有序集合對象
有序集合的編碼可以是ziplist或者skiplist。
ziplist編碼的壓縮列表對象使用壓縮列表作爲底層實現,每個集合元素使用兩個緊挨在一起的壓縮列表節點來保存,第一個節點保存元素的成員(member),而第二個節點則保存元素的分值(score)。
壓縮列表內的集合元素則按分值從小到大進行排序,分值較小的元素被放置在靠近表頭的方向,而分值較大的元素則被放置在靠近表尾的方向。
typedef struct zset {
zskiplist *zsl;
dict *dict;
}

在這裏插入圖片描述
在這裏插入圖片描述

爲什麼有序集合需要同時使用跳躍表和字典實現?
在理論上,有序集合可以單獨使用字典或者跳躍表的其中一種數據結構來實現,但無論單獨使用字典還是跳躍表,在性能上對比起同時使用字典和跳躍表都會有所降低。單獨使用跳躍表,O(1)複雜度查找成員的分值這一特性就沒了,單獨使用字典,O(1)範圍查詢的特性就沒了。
有序集合的每個元素的成員都是一個字符串對象,而每個元素的分值都是一個double類型的浮點數。值得一提的是,雖然zset結構同時使用跳躍表和字典來保存有序集合元素,但這兩種數據結構都會通過指針來共享相同元素的成員和分值,所以同時使用跳躍表和字典來保存集合元素不會產生任何重複成員或者分值,也不會因此而浪費額外的內存。
在這裏插入圖片描述

編碼轉換:
當列表對象可以同時滿足以下兩個條件時,列表對象使用ziplist編碼:
1.列表對象保存的所有字符串元素的長度都小於64字節,
2.列表對象保存的元素數量小於512個
其它使用hashtable編碼
注意
以上兩個條件的上限值是可以修改的,具體請看配置文件中關於zset-max-ziplist-value選項和zset-max-ziplist-entries選項的說明。

有序集合命令的實現
在這裏插入圖片描述

類型檢查與命令多態
Redis中用於操作鍵的命令基本上可以分爲兩種類型。一種是可以操作任何類型的鍵執行,比如說DEL、EXPIRE、RENAME、TYPE、OBJECT命令;另一種是隻能對特定類型的鍵執行,比如說:
在這裏插入圖片描述

類型檢查的實現
在執行一個類型特定的命令之前,Redis會檢查輸入鍵的類型是否正確,然後再決定是否執行給定的命令。類型特定命令所進行的類型檢查是通過redisObject結構的type屬性來實現的:在執行一個類型特定命令之前,服務器會先檢查輸入數據庫鍵的值對象是否爲執行命令所需的類型,如果是的,服務器就對鍵執行特定的命令;否則,服務器拒絕執行命令。

多態命令的實現
Redis除了會根據值對象的類型來判斷鍵是否能夠執行指定命令之外,還會根據值對象的編碼方式,選擇正確的命令實現代碼來執行命令。DEL、EXPIRE、TYPE等命令也稱爲基於類型的多態(一個命令可以同時用於處理多種不同類型的鍵),而後者是基於編碼的多態(一個命令可以同時用於處理多種不同的編碼)。
在這裏插入圖片描述

內存回收
c語言不具備自動內存回收功能,所以Redis在自己的對象系統中構建了一個引用計數器(reference conunting)技術實現內存回收機制,通過這一機制,程序可以通過跟蹤對象的引用技術信息,在適當的時候自動釋放對象並進行內存回收。
typedef struct redisObject {
//引用計數
int refcount;
} robj;
在這裏插入圖片描述

對象共享
除了用於實現引用計數內存回收機制之外,對象的引用技術屬性還帶有對象共享的作用。
讓多個鍵共享同一個值對象需要執行以下兩個步驟:
1.將數據庫鍵的指針指向同一個現有的值對象
2.將被共享的值對象的引用計數增一
共享的越多,節省的內存越多。
Redis會在初始化服務器的時候,創建了一萬個字符串對象,這些對象包含了從0到9999的所有整數值,服務器需要去哦用到0-9999的字符串對象時,會共享對象,而不是新創建對象。
注意: 創建共享字符串對象的數量可以通過修改redis.h/REDIS_SHARED_INTEGERS常量來修改

set A 100 有一個共享 set B 100 有兩個共享
在這裏插入圖片描述

儘管共享更復雜的對象可以節約更多的內存,但受到CPU時間的限制,Redis只對包含整數值的字符串對象進行共享。

對象的空轉時長
redisObject結構包含的最後一個屬性爲lru屬性,該屬性記錄了對象最後一次被程序訪問的時間:
typedef struct redisObject {
unsigned lru:22;
} robj;
OBJECT IDLETIME命令可以打印出給定鍵的空轉時長。這一空轉時長就是通過將當前時間減去鍵的值對象的lru時間計算得出的。
如果服務器打開了maxmemory選項,並且服務器用於回收內存的算法爲volatile-lru或者allkeys-lru,那麼當服務器佔用的內存數超過了maxmemory選項設置的上限值是,空轉時長較高的部分鍵會優先被服務器釋放,從而回收內存。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章