Redis系列(十)Redis對象系統

前言

其實關於本文,我猶豫再三。

  1. 對象系統值得寫一篇文章嗎?從技術上來講,當然是值。但是對於我們大部分人來說,它都是隱身的。
  2. 寫的話,順序放在哪裏?在 Redis 系列(九)底層數據結構之五種基礎數據類型的實現中其實就提到了,那麼應該在此之前先介紹它嗎?

結論:想那麼多屁事,寫就完事了。

介紹

正如上一篇文章提到的,Redis 不是生硬的使用前面介紹過的數據結構,來實現了字符串,列表,字典等等數據結構,而是精心打造了一個對象系統。

對於 Redis 來說,所有的所謂的數據類型,本質上都是一個對象,而且同一個類型的對象,底層實現編碼不一樣。

Redis 對象的定義爲:

// 類型
typedef type:4;
// 編碼
unsigned encoding:4;
// 指向底層數據結構的指針
void *ptr;
...

類型

對象的 type 屬性,記錄了對象的類型,這個類型就是我們所熟知的 Reids 的數據類型了,比如字符串,列表,集合,有序集合,散列等。

對於 Redis 數據庫中的鍵值對來講,鍵值永遠是一個字符串對象,值可以是很多種。

編碼和底層數據結構

對象的 ptr 指針,指向對象的底層數據結構,而這個數據結構是什麼,則由 encoding 來決定。它可以是以下任意一種:

  • REDIS_ENCODING_INT
    long 類型的整數
  • REDIS_ENCODING_ENBSTR
    embstr 編碼的簡單動態字符串(不知道的可以去看上一篇文章)
  • REDIS_ENCODING_RAW
    簡單動態字符串
  • REDIS_ENCODING_HT
    字典
  • REDIS_ENCODING_LINKEDLIST
    雙端鏈表
  • REDIS_ENCODING_ZIPLIST
    壓縮列表
  • REDIS_ENCODING_INTSET
    整數集合
  • REDIS_ENCODING_SKIPLIST
    跳躍表和字典

每種類型的對象都至少可以使用兩種不同的編碼。如下表:

2020-01-16-20-48-29

五種常見的對象類型

對於我們而言,工作中最常用以及面試中最常被問到的五種數據類型,他們的底層分別使用了什麼編碼及數據結構,多種編碼之間的切換條件是怎樣的?

這些問題你都可以在上一篇文章中找到答案。敬請查看 Redis 系列(九)底層數據結構之五種基礎數據類型的實現

類型檢查與命令多態

如果讀者熟悉 Redis 的命令的話(不熟沒關係,看下一篇文章), 就會發現,Redis 的命令設計維度不是單一的。

比如有一類命令只能對指定的 數據類型執行。比如 ZADD 及各種 ADD.

而有一些命令是可以對所有類型操作的,比如 TYPE DEL 等等。

爲了確保命令可以被正確的執行,Redis 需要進行命令的檢查,因爲相信用戶不會亂用是十分愚蠢的。

在所有命令被執行之前,Redis 會首先檢查輸入的鍵的類型是否與命令匹配,這個檢查就是應用 redisObject 中的 type字段進行的。

如果匹配,則繼續執行命令,如果不匹配則返回特定的錯誤信息。

除了進行類型檢查之外,Redis 還應用對象的類型進行命令的多態。

設想一下,列表對象可以 使用LLEN命令來求出當前元素的個數,而在以前,列表對象的實現可能是壓縮列表,也可以是雙端鏈表,那麼對於他們而言,求出長度的方法當然是不一樣的。

Redis 會首先進行類型檢查,之後根據當前對象的編碼來決定當前命令應該調用哪個數據結構的 API. 以此來實現命令的多態。

內存回收

學習 Java 的同志們看到這裏是不是倍感親切,彷彿看到了家人。

衆所周知,c 語言是沒有自動化的內存管理的,但是 Redis 這麼大的系統又不可能完全手動的控制內存使用,因此需要一套自動化的內存回收機制。

Redis 在自己的對象系統中,基於引用計數實現了內存回收。

在 redisObject 對象中,還有一個額外的書序 refcount.

  • 創建對象時,引用計數爲 1.
  • 當對象被一個新程序使用時,引用計數+1.
  • 當對象被一個程序拋棄的時候,引用計數-1;
  • 當對象的引用計數爲 0, 對象會被回收,它所佔用的內存被釋放掉。

對於這一塊的具體實現我也沒看,但是引用計數的原理想必各位都很清楚了,如果不清楚的話隨便 google 一下JVM 內存回收基本上都會順手講到引用計數的。

對象共享

除了用於使用基於引用計數的內存回收之外,對象的引用計數屬性,還被用來做一些對象共享的工作。

設想一下,首先你創建了一個 kye=a, value=100的對象,過一會你又創建了一個key=b, value=100的對象,如此循環往復。內存會無線增大,但是其實保存的是同一個信息。

這些對象理論上來講是完全可以進行共享的,即,首先我創建一個value=100的對象放在這裏,每當你新創建一個上面那樣的對象時,我就把指針指過來就好了。

Redis 有選擇性的這樣子做了,當它共享之前,會先給對應的對象的引用計數+1, 之後把指針指過來。

爲什麼說是有選擇性的呢?因爲 Redis 只會緩存0-9999的數字字符串,如果你創建的鍵值對的值是這個,Redis 就會直接使用共享對象了。

爲什麼不多緩存一點呢?最好是把系統中所有相同的值全緩存起來,這樣子最省內存了。Redis 不是最缺內存了嗎?

是的,這樣子當然是省內存,但是** Redis 是一個高性能的內存數據庫**.

性能這一塊,Redis 卡的死死的。

想要判斷兩個對象的值是否相同,如果都是整數,只需要 O(1). 如果都是字符串,那麼需要 O(N). 如果都是複雜對象(比如 hash), 那麼可能需要 O(N2). Redis 爲了更好的性能,放棄了緩存更加複雜的對象。

對象淘汰:空轉時長

RedisObject 還有一個屬性,unsigned lru:32;.

從名字我們就可以看出來它是做什麼的了,它記錄了當前對象最後一次被訪問的時間。

這個時間會在 Redis 的內存使用滿了之後,Redis 會進行對象的淘汰,其中有一種算法是LRU. 會用到對象上一次被訪問的時間。

同時,我們也可以手動的查看某一個對象的空轉時長。空轉時長=當前時間-最後一次訪問時間.

2020-01-16-21-19-56

總結

這篇文章大概了講了一下 Redis 中的對象系統設計,及對象系統可以用來做什麼。

  • 可以爲數據類型的多種實現方式提供土壤
  • 類型檢查與命令多態
  • 基於引用計數的內存回收
  • 對象共享,節省內存
  • 記錄對象的空轉時長,用於 LRU 等。

參考文章

《Redis 的設計與實現(第二版)》


完。

聯繫我

最後,歡迎關注我的個人公衆號【 呼延十 】,會不定期更新很多後端工程師的學習筆記。
也歡迎直接公衆號私信或者郵箱聯繫我,一定知無不言,言無不盡。


以上皆爲個人所思所得,如有錯誤歡迎評論區指正。

歡迎轉載,煩請署名並保留原文鏈接。

聯繫郵箱:[email protected]

更多學習筆記見個人博客或關注微信公衆號 < 呼延十 >------>呼延十

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