redisObject 數據結構,以及 Redis 的數據類型
redisObject 是 Redis 類型系統的核心, 數據庫中的每個鍵、值,以及 Redis 本身處理的參數, 都表示為這種數據類型。
redisObject 的定義位於 redis.h :
<code>/*
* Redis 對象
*/
typedef struct redisObject {
// 類型
unsigned type:4;
// 對齊位
unsigned notused:2;
// 編碼方式
unsigned encoding:4;
// LRU 時間(相對於 server.lruclock)
unsigned lru:22;
// 引用計數
int refcount;
// 指向對象的值
void *ptr;
} robj;
/<code>
type 、 encoding 和 ptr 是最重要的三個屬性。
type 記錄了對象所保存的值的類型,它的值可能是以下常量的其中一個(定義位於 redis.h):
<code>/*
* 對象類型
*/
#define REDIS_STRING 0 // 字符串
#define REDIS_LIST 1 // 列表
#define REDIS_SET 2 // 集合
#define REDIS_ZSET 3 // 有序集
#define REDIS_HASH 4 // 哈希表
/<code>
encoding 記錄了對象所保存的值的編碼,它的值可能是以下常量的其中一個(定義位於 redis.h):
<code>/*
* 對象編碼
*/
#define REDIS_ENCODING_RAW 0 // 編碼為字符串
#define REDIS_ENCODING_INT 1 // 編碼為整數
#define REDIS_ENCODING_HT 2 // 編碼為哈希表
#define REDIS_ENCODING_ZIPMAP 3 // 編碼為 zipmap
#define REDIS_ENCODING_LINKEDLIST 4 // 編碼為雙端鏈表
#define REDIS_ENCODING_ZIPLIST 5 // 編碼為壓縮列表
#define REDIS_ENCODING_INTSET 6 // 編碼為整數集合
#define REDIS_ENCODING_SKIPLIST 7 // 編碼為跳躍表
/<code>
ptr 是一個指針,指向實際保存值的數據結構,這個數據結構由 type 屬性和 encoding 屬性決定。
舉個例子,如果一個 redisObject 的 type 屬性為 REDIS_LIST , encoding 屬性為 REDIS_ENCODING_LINKEDLIST ,那麼這個對象就是一個 Redis 列表,它的值保存在一個雙端鏈表內,而 ptr 指針就指向這個雙端鏈表;
另一方面,如果一個 redisObject 的 type 屬性為 REDIS_HASH , encoding 屬性為 REDIS_ENCODING_ZIPMAP ,那麼這個對象就是一個 Redis 哈希表,它的值保存在一個 zipmap 裡,而 ptr 指針就指向這個 zipmap ;諸如此類。
下圖展示了 redisObject 、Redis 所有數據類型、以及 Redis 所有編碼方式(底層實現)三者之間的關係:
命令的類型檢查和多態
有了 redisObject 結構的存在, 在執行處理數據類型的命令時, 進行類型檢查和對編碼進行多態操作就簡單得多了。
當執行一個處理數據類型的命令時, Redis 執行以下步驟:
- 根據給定 key ,在數據庫字典中查找和它相對應的 redisObject ,如果沒找到,就返回 NULL 。
- 檢查 redisObject 的 type 屬性和執行命令所需的類型是否相符,如果不相符,返回類型錯誤。
- 根據 redisObject 的 encoding 屬性所指定的編碼,選擇合適的操作函數來處理底層的數據結構。
- 返回數據結構的操作結果作為命令的返回值。
作為例子,以下展示了對鍵 key 執行 LPOP 命令的完整過程:
對象共享
有一些對象在 Redis 中非常常見, 比如命令的返回值 OK 、 ERROR 、 WRONGTYPE 等字符, 另外,一些小範圍的整數,比如個位、十位、百位的整數都非常常見。
為了利用這種常見情況, Redis 在內部使用了一個 Flyweight 模式 :通過預分配一些常見的值對象, 並在多個數據結構之間共享這些對象, 程序避免了重複分配的麻煩, 也節約了一些 CPU 時間。
Redis 預分配的值對象有如下這些:
各種命令的返回值,比如執行成功時返回的 OK ,執行錯誤時返回的 ERROR ,類型錯誤時返回的 WRONGTYPE ,命令入隊事務時返回的 QUEUED ,等等。
包括 0 在內,小於 redis.h/REDIS_SHARED_INTEGERS 的所有整數(REDIS_SHARED_INTEGERS 的默認值為 10000)
因為命令的回覆值直接返回給客戶端, 所以它們的值無須進行共享;另一方面, 如果某個命令的輸入值是一個小於 REDIS_SHARED_INTEGERS 的整數對象, 那麼當這個對象要被保存進數據庫時, Redis 就會釋放原來的值, 並將值的指針指向共享對象。
作為例子,下圖展示了三個列表,它們都帶有指向共享對象數組中某個值對象的指針:
三個列表的值分別為:
<code>列表 A :[20130101, 300, 10086] ,
列表 B :[81, 12345678910, 999] ,
列表 C :[100, 0, -25, 123] 。
/<code>
引用計數以及對象的銷燬
當將 redisObject 用作數據庫的鍵或者值, 而不是用來儲存參數時, 對象的生命期是非常長的, 因為 C 語言本身沒有自動釋放內存的相關機制, 如果只依靠程序員的記憶來對對象進行追蹤和銷燬, 基本是不太可能的。
另一方面,正如前面提到的,一個共享對象可能被多個數據結構所引用, 這時像是“這個對象被引用了多少次?”之類的問題就會出現。
為了解決以上兩個問題, Redis 的對象系統使用了引用計數技術來負責維持和銷燬對象, 它的運作機制如下:
每個 redisObject 結構都帶有一個 refcount 屬性,指示這個對象被引用了多少次。
當新創建一個對象時,它的 refcount 屬性被設置為 1 。
當對一個對象進行共享時,Redis 將這個對象的 refcount 增一。
當使用完一個對象之後,或者取消對共享對象的引用之後,程序將對象的 refcount 減一。
當對象的 refcount 降至 0 時,這個 redisObject 結構,以及它所引用的數據結構的內存,都會被釋放。
小結
Redis 使用自己實現的對象機制來實現類型判斷、命令多態和基於引用計數的垃圾回收。
一種 Redis 類型的鍵可以有多種底層實現。
Redis 會預分配一些常用的數據對象,並通過共享這些對象來減少內存佔用,和避免頻繁地為小對象分配內存。
閱讀更多 sandag 的文章