Redis底層探秘(六):對象多态及回收
本篇是我們redis系列的最後一篇,整個系列其實是我學習《redis設計與實作》的筆記,這本書感覺不錯,推薦使用redis的小夥伴都可以看看。
整個系列的文字都比較幹,很多資料結構和C語言的東西,不過隻有這麼接近底層,我們才能知道redis為什麼可以做到這麼塊。
類型檢查與指令多态
redis中用于操作鍵的指令基本上可以分為兩種類型。
其中一種指令可以對任何類型的鍵執行,比如說del指令、expire指令、rename指令、type指令、object指令等。
而另一種指令隻能對特定類型的鍵執行,比如set、get、append、strlen等指令隻能對字元串鍵鍵執行;hdel、hset、hget、hlen等指令隻能對哈希鍵執行。
類型檢查的實作
類型特定指令鎖進行的類型檢查是通過redisObject結構的type屬性來實作的:
1 在執行一個類型特定指令之前,伺服器會先檢查輸入資料庫鍵的值對象是否為執行指令所需的類型,如果是的話,伺服器就對鍵執行指定的指令;
2 否則,伺服器将拒絕執行指令,并向用戶端傳回一個類型錯誤。

多态指令的實作
redis除了會根據值對象的類型來判斷鍵是否能夠執行指定指令之外,還會根據值對象的編碼方式,選擇正确的指令實作代碼來執行指令。
舉個例子,在前面介紹清單對象的編碼時我們說過,清單對象有ziplist和linkedlist兩種編碼可用,其中前者使用壓縮清單api來實作清單指令,而後者使用雙端連結清單api來實作清單指令。
現在考慮這樣一個情況,如果我們隊一個鍵執行llen指令,那麼伺服器除了要確定執行指令的是清單鍵之外,還需要根據鍵的值對象所使用的編碼來選擇正确的llen指令實作:
1 如果清單對象的編碼為ziplist,那麼說明清單對象的實作為壓縮清單,程式将使用ziplistLen函數來傳回清單的長度;
2 如果清單對象的編碼為linkedlist,那麼說明清單對象的實作為雙端連結清單,程式将使用listLength函數來傳回雙端連結清單的長度
借用面向對象方面的術語來說,我們可以認為llen指令是多态(polymorphism),隻要執行llen指令的是清單鍵,那麼無論值對象使用的是ziplist編碼還是linkedlist編碼,指令都可以正常執行。
記憶體回收
因為C語言并不具備自動記憶體回收功能,是以redis在自己的對象系統中建構了一個引用計數技術實作的記憶體回收機制,通過這一機制,程式可以通過跟中對象的引用計數資訊,在适當的時候自動釋放對象并進行記憶體回收。
每個對象的引用計數資訊由redisObject結構的refcount屬性記錄:
typedef struct redisObject{
// …
//引用計數
int refcount;
…….
}
對象的引用計數資訊會随着對象的使用狀态而不斷變化:
1 在建立一個新對象時,引用計數的值會被初始化為1
2 當對象呗一個新程式使用時,他的引用計數值會被增一
3 當對象不再被一個程式使用時,他的引用計數值會被增一
4 當對象的引用計數值變為0時,對象所占用的記憶體會被釋放
對象的整個生命周期可以劃分為建立對象、操作對象、釋放對象三個階段。
對象共享
除了用于實作引用計數記憶體回收機制之外,對象的引用計數屬性還帶有對象共享的作用。舉個例子,假設鍵A建立了一個包含整數值100的字元串對象作為值對象,如果這時鍵B也要建立一個同樣儲存了整數值100的字元串對象作為值對象,那麼伺服器有以下兩種做法:
1 為鍵B新建立一個包含整數值100的字元串對象
2 讓鍵A和和鍵B共享同一個字元串對象
很明顯,第二種方法更節約記憶體,在redis中,讓多個鍵共享一個值對象需要執行以下兩個步驟
1 将資料庫鍵的值指針指向一個現有的值對象、
2 将被共享的值對象的引用計數增一
目前來說,redis會在初始化伺服器時,建立一萬個字元串對象,這些對象包含了從0到9999的所有整數值,當伺服器需要用到值為0到9999的字元串對象時,伺服器就會使用這些共享對象,而不是新建立對象。(可以通過配置修改常量值)
另外,這些共享對象不單單隻有字元串字元串鍵可以使用,那些在資料結構中嵌套了字元串對象的對象(linkedlist編碼的清單對象、hashtable編碼的哈希對象等)都可以使用這些共享對象。
延伸閱讀:為什麼redis不共享包含字元串的對象?
當伺服器考慮将一個共享對象設定為鍵的值對象時,程式需要先檢查給定的共享對象和鍵想建立的目标是否完全相同,隻有在共享對象和目标對象完全相同的情況下,程式才會将共享對象用作鍵的值對象,而一個共享對象儲存的值越複雜,驗證共享對象和目标對象是否相同所需要的複雜度就會越高,消耗的cpu時間也會越多。
1 如果共享對象是儲存整數值的字元串對象,那麼驗證操作的複雜度為o(1)
2 如果共享對象是儲存字元串值的字元串對象,那麼驗證操作的複雜度為O(N)
3 如果共享對象對象是包含了多個值(或者對象的)對象,比如清單對象或者哈希對象,那麼驗證操作的複雜度将會是O(N^2)
是以,盡管共享更複雜的對象可以節約更多的記憶體,但受到cpu時間的限制,redis隻對包含整數值的字元串對象進行共享。
對象的空轉時長
除了前面介紹介紹過的type、encoding、ptr和refcount四個屬性值之外,redisObject結構包含的最後一個屬性為lru屬性,該屬性記錄了對象最後一次被指令程式通路的時間,這個時間被利用在對象回收算法中。
對象回顧
1 redis資料庫中的每個鍵值對的鍵和值都是一個對象
2 redis公有字元串、清單、哈希、集合、有序集合五種類型的對象,每種類型的對象至少都有兩種或以上的編碼方式,不同的編碼可以在不同的使用場景上優化對象的使用效率。
3 伺服器在執行某些指令之前,會先檢查給定鍵的類型能否執行指定的指令,而檢查一個鍵的類型就是檢查鍵的值對象的類型。
4 redis的對象系統帶有引用計數實作的記憶體回收機制,當一個對象不再被使用時,該對象所占用的記憶體就會被自動釋放。
5 redis 會共享值為0到9999的字元串對象。
6 對象會記錄自己的最後一次被通路的時間,這個時間可以用于計算對象的空轉時間。