簡介
redisObject:即redis對象临庇,redis數(shù)據(jù)庫是以Key-Value形式存在假夺,當(dāng)新建一個Key-Value對時,至少會創(chuàng)建兩個對象已卷,一個用于作為Key對象侧蘸,一個用于作為Value對象讳癌,每個對象都由一個redisObject的結(jié)構(gòu)表示。
數(shù)據(jù)結(jié)構(gòu)
redisObject數(shù)據(jù)結(jié)構(gòu)如下(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對象所保存的值對應(yīng)編碼埋虹,也就是數(shù)據(jù)結(jié)構(gòu)娩怎。
#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記錄了對象空轉(zhuǎn)時長截亦,OBJECT IDLETIME 命令可以打印出給定鍵的空轉(zhuǎn)時長。
redis> SET key1 "hello world"
OK
# 過段時間執(zhí)行
redis> OBJECT IDLETIME key1
(integer) 20
OBJECT IDLETIME命令在訪問鍵的值對象時袍啡, 不會更新值對象的 lru 屬性境输。
如果服務(wù)器打開了 maxmemory 選項颖系, 并且服務(wù)器用于回收內(nèi)存的算法為 volatile-lru 或者 allkeys-lru 嘁扼, 那么當(dāng)服務(wù)器占用的內(nèi)存數(shù)超過了 maxmemory 選項所設(shè)置的上限值時, 空轉(zhuǎn)時長較高的那部分鍵會優(yōu)先被服務(wù)器釋放强缘, 從而回收內(nèi)存旅掂。
refcount
Redis中為了更好的優(yōu)化內(nèi)存空間访娶,對數(shù)字字符串進(jìn)行了共享內(nèi)存的操作,并以引用計數(shù)方式進(jìn)行管理(object.c)
// 將對象的引用計數(shù)值設(shè)置為 0 , 但并不釋放對象戳晌, 這個函數(shù)通常在需要重新設(shè)置對象的引用計數(shù)值時使用。
robj *resetRefCount(robj *obj) {
obj->refcount = 0;
return obj;
}
// 將對象的引用計數(shù)值增一
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");
}
}
}
// 將對象的引用計數(shù)值減一, 當(dāng)對象的引用計數(shù)值等于 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 ;
- 對一個對象進(jìn)行共享時囊嘉,這個對象的 refcount +1扭粱;
- 當(dāng)使用完一個對象之后震檩,或者取消對共享對象的引用之后,程序?qū)ο蟮?refcount -1博其;
- 當(dāng)對象的 refcount 降至 0 時贺奠,釋放redisObject 對象及*ptr引用的數(shù)據(jù)結(jié)構(gòu)错忱。
*ptr
*ptr 是一個指針以清,指向?qū)嶋H保存值的數(shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)由 type 屬性和 encoding 屬性決定眉孩。
假設(shè)redisObject 的 type = OBJ_ZSET勒葱, encoding = OBJ_ENCODING_SKIPLIST 凛虽,代表當(dāng)前redisObject是一個基于采用跳躍列表數(shù)據(jù)結(jié)構(gòu)的有序集合凯旋,*ptr指向?qū)?yīng)的跳躍列表钉迷。
類型檢查和命令多態(tài)
當(dāng)執(zhí)行一個處理數(shù)據(jù)類型的命令時糠聪, Redis 執(zhí)行以下步驟:
- 根據(jù)給定 key 舰蟆,在數(shù)據(jù)庫字典中查找和它相對應(yīng)的 redisObject 夭苗,如果沒找到,就返回 NULL 题造。
- 判斷 redisObject 的 type屬性 = 執(zhí)行命令所需的類型(HSET) 界赔?牵触,如果不相符揽思,返回類型錯誤。
- 根據(jù) redisObject 的 encoding 屬性所指定的編碼钉汗,選擇合適的操作函數(shù)來處理底層的數(shù)據(jù)結(jié)構(gòu)损痰。
- 返回數(shù)據(jù)結(jié)構(gòu)的操作結(jié)果作為命令的返回值卢未。
因此辽社,上述第2步就是類型檢查滴铅,第3步就是命令多態(tài)。
對象共享
除了用于實現(xiàn)引用計數(shù)內(nèi)存回收機制之外譬淳, 對象的引用計數(shù)屬性還帶有對象共享的作用邻梆。
假設(shè)鍵 key1創(chuàng)建了一個包含整數(shù)值 1 的字符串對象作為值對象浦妄,這時鍵key2也要創(chuàng)建一個同樣保存了整數(shù)值 1 的字符串對象作為值對象见芹,為了讓多個鍵共享同一個值對象需要執(zhí)行以下兩個步驟:
- 將數(shù)據(jù)庫鍵的值指針指向一個現(xiàn)有的值對象玄呛;
- 將被共享的值對象的引用計數(shù)增一徘铝。
目前來說惕它, Redis 會在初始化服務(wù)器時, 創(chuàng)建一萬個字符串對象郁惜, 這些對象包含了從 0 到 9999 的所有整數(shù)值兆蕉, 當(dāng)服務(wù)器需要用到值為 0到 9999 的字符串對象時恨樟, 服務(wù)器就會使用這些共享對象疚俱, 而不是新創(chuàng)建對象呆奕。
創(chuàng)建共享字符串對象的數(shù)量可以通過修改 server.h/OBJ_SHARED_INTEGERS常量來修改梁钾。
redis> SET key1 1
OK
redis> OBJECT REFCOUNT key1
(integer) 2
該引用計數(shù)=2绳泉,是因為服務(wù)器程序初始化時,內(nèi)置了該對象了姆泻,refcount=1零酪。
另外冒嫡, 這些共享對象不單單只有字符串鍵可以使用, 那些在數(shù)據(jù)結(jié)構(gòu)中嵌套了字符串對象的對象(linkedlist
編碼的列表對象四苇、 hashtable
編碼的哈希對象孝凌、 hashtable
編碼的集合對象、以及 zset
編碼的有序集合對象)都可以使用這些共享對象月腋。
為什么 Redis 不共享包含字符串的對象蟀架?
當(dāng)服務(wù)器考慮將一個共享對象設(shè)置為鍵的值對象時, 程序需要先檢查給定的共享對象
和鍵想創(chuàng)建的目標(biāo)對象
是否完全相同榆骚, 只有在共享對象
和目標(biāo)對象
完全相同的情況下片拍, 程序才會將共享對象
用作鍵的值對象, 而一個共享對象
保存的值越復(fù)雜, 驗證共享對象
和目標(biāo)對象
是否相同所需的復(fù)雜度就會越高, 消耗的 CPU 時間也會越多:
- 如果
共享對象
是保存整數(shù)值的字符串對象色徘, 那么驗證操作的復(fù)雜度為 O(1);- 如果
共享對象
是保存字符串值的字符串對象斤寂, 那么驗證操作的復(fù)雜度為 O(N)器腋;- 如果
共享對象
是包含了多個值(或者對象的)對象诊县, 比如列表對象或者哈希對象怎披, 那么驗證操作的復(fù)雜度將會是 O(N^2) 缴渊。
因此, 盡管共享更復(fù)雜的對象可以節(jié)約更多的內(nèi)存, 但受到 CPU 時間的限制, Redis 只對包含整數(shù)值的字符串對象進(jìn)行共享搓劫。