簡介
redisObject:即redis對象,redis數據庫是以Key-Value形式存在,當新建一個Key-Value對時,至少會創建兩個對象,一個用于作為Key對象,一個用于作為Value對象,每個對象都由一個redisObject的結構表示。
數據結構
redisObject數據結構如下(server.h):
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
type
表示redis對象所保存的值的類型。
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
encoding
表示redis對象所保存的值對應編碼,也就是數據結構。
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
lru
lru記錄了對象空轉時長,OBJECT IDLETIME 命令可以打印出給定鍵的空轉時長。
redis> SET key1 "hello world"
OK
# 過段時間執行
redis> OBJECT IDLETIME key1
(integer) 20
OBJECT IDLETIME命令在訪問鍵的值對象時, 不會更新值對象的 lru 屬性。
如果服務器打開了 maxmemory 選項, 并且服務器用于回收內存的算法為 volatile-lru 或者 allkeys-lru , 那么當服務器占用的內存數超過了 maxmemory 選項所設置的上限值時, 空轉時長較高的那部分鍵會優先被服務器釋放, 從而回收內存。
refcount
Redis中為了更好的優化內存空間,對數字字符串進行了共享內存的操作,并以引用計數方式進行管理(object.c)
// 將對象的引用計數值設置為 0 , 但并不釋放對象, 這個函數通常在需要重新設置對象的引用計數值時使用。
robj *resetRefCount(robj *obj) {
obj->refcount = 0;
return obj;
}
// 將對象的引用計數值增一
void incrRefCount(robj *o) {
if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT) {
o->refcount++;
} else {
if (o->refcount == OBJ_SHARED_REFCOUNT) {
/* Nothing to do: this refcount is immutable. */
} else if (o->refcount == OBJ_STATIC_REFCOUNT) {
serverPanic("You tried to retain an object allocated in the stack");
}
}
}
// 將對象的引用計數值減一, 當對象的引用計數值等于 0 時, 釋放對象。
void decrRefCount(robj *o) {
if (o->refcount == 1) {
switch(o->type) {
case OBJ_STRING: freeStringObject(o); break;
case OBJ_LIST: freeListObject(o); break;
case OBJ_SET: freeSetObject(o); break;
case OBJ_ZSET: freeZsetObject(o); break;
case OBJ_HASH: freeHashObject(o); break;
case OBJ_MODULE: freeModuleObject(o); break;
case OBJ_STREAM: freeStreamObject(o); break;
default: serverPanic("Unknown object type"); break;
}
zfree(o);
} else {
if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--;
}
}
- 新建一個對象時,它的 refcount = 1 ;
- 對一個對象進行共享時,這個對象的 refcount +1;
- 當使用完一個對象之后,或者取消對共享對象的引用之后,程序將對象的 refcount -1;
- 當對象的 refcount 降至 0 時,釋放redisObject 對象及*ptr引用的數據結構。
*ptr
*ptr 是一個指針,指向實際保存值的數據結構,這個數據結構由 type 屬性和 encoding 屬性決定。
假設redisObject 的 type = OBJ_ZSET, encoding = OBJ_ENCODING_SKIPLIST ,代表當前redisObject是一個基于采用跳躍列表數據結構的有序集合,*ptr指向對應的跳躍列表。
類型檢查和命令多態
當執行一個處理數據類型的命令時, Redis 執行以下步驟:
- 根據給定 key ,在數據庫字典中查找和它相對應的 redisObject ,如果沒找到,就返回 NULL 。
- 判斷 redisObject 的 type屬性 = 執行命令所需的類型(HSET) ?,如果不相符,返回類型錯誤。
- 根據 redisObject 的 encoding 屬性所指定的編碼,選擇合適的操作函數來處理底層的數據結構。
- 返回數據結構的操作結果作為命令的返回值。
因此,上述第2步就是類型檢查,第3步就是命令多態。
對象共享
除了用于實現引用計數內存回收機制之外, 對象的引用計數屬性還帶有對象共享的作用。
假設鍵 key1創建了一個包含整數值 1 的字符串對象作為值對象,這時鍵key2也要創建一個同樣保存了整數值 1 的字符串對象作為值對象,為了讓多個鍵共享同一個值對象需要執行以下兩個步驟:
- 將數據庫鍵的值指針指向一個現有的值對象;
- 將被共享的值對象的引用計數增一。
目前來說, Redis 會在初始化服務器時, 創建一萬個字符串對象, 這些對象包含了從 0 到 9999 的所有整數值, 當服務器需要用到值為 0到 9999 的字符串對象時, 服務器就會使用這些共享對象, 而不是新創建對象。
創建共享字符串對象的數量可以通過修改 server.h/OBJ_SHARED_INTEGERS常量來修改。
redis> SET key1 1
OK
redis> OBJECT REFCOUNT key1
(integer) 2
該引用計數=2,是因為服務器程序初始化時,內置了該對象了,refcount=1。
另外, 這些共享對象不單單只有字符串鍵可以使用, 那些在數據結構中嵌套了字符串對象的對象(linkedlist
編碼的列表對象、 hashtable
編碼的哈希對象、 hashtable
編碼的集合對象、以及 zset
編碼的有序集合對象)都可以使用這些共享對象。
為什么 Redis 不共享包含字符串的對象?
當服務器考慮將一個共享對象設置為鍵的值對象時, 程序需要先檢查給定的共享對象
和鍵想創建的目標對象
是否完全相同, 只有在共享對象
和目標對象
完全相同的情況下, 程序才會將共享對象
用作鍵的值對象, 而一個共享對象
保存的值越復雜, 驗證共享對象
和目標對象
是否相同所需的復雜度就會越高, 消耗的 CPU 時間也會越多:
- 如果
共享對象
是保存整數值的字符串對象, 那么驗證操作的復雜度為 O(1);- 如果
共享對象
是保存字符串值的字符串對象, 那么驗證操作的復雜度為 O(N);- 如果
共享對象
是包含了多個值(或者對象的)對象, 比如列表對象或者哈希對象, 那么驗證操作的復雜度將會是 O(N^2) 。
因此, 盡管共享更復雜的對象可以節約更多的內存, 但受到 CPU 時間的限制, Redis 只對包含整數值的字符串對象進行共享。