1. 對象類型和內部編碼
我們之前分析了幾種redis底層的數據結構,包括簡單字符串、雙端鏈表、字典、跳錶、整數集合、壓縮列表等,這些還不是redis的對象類型,redis的對象類型總共包含5種,分別是字符串、列表、集合、有序集合、哈希,在server.h
中定義。
/* 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. */
每種對象類型在底部都有不同的實現方式,而且不止一種。
但是在執行命令時,不需要關注key的底層實現是哪一種,無論是哪一種,都會自動的調用相關的處理函數,這類似多態,要求redis在內部能夠執行類型檢查等操作。
Redis 的每一種數據類型,比如字符串、列表、有序集,它們都擁有不只一種底層實現(Redis 內部稱之爲編碼,encoding),這說明,每當對某種數據類型的鍵進行操作時,程序都必須根據鍵所採取的編碼,進行不同的操作。
比如說,集合類型就可以由字典和整數集合兩種不同的數據結構實現,但是,當用戶執行ZADD 命令時,他/她應該不必關心集合使用的是什麼編碼,只要Redis 能按照ZADD 命令的指示,將新元素添加到集合就可以了。
這說明,操作數據類型的命令除了要對鍵的類型進行檢查之外,還需要根據數據類型的不同編碼進行多態處理。——《redis設計與實現》
那麼五種數據類型的底層實現到底是什麼呢?
可以通過下面的圖大致看一下,redis-5.0.8版本和下面的圖有些差異:
1.1 字符串內部編碼
encoding | 含義 |
---|---|
OBJ_ENCODING_INT | 整數 |
OBJ_ENCODING_RAW | sds動態字符串 |
OBJ_ENCODING_EMBSTR | Embedded sds |
Embedded sds 其實就是sdshdr8。在字符串較短(長度小於44字節)的情況下會使用這種編碼方式。至於爲什麼是小於44字節的時候採用Embedded sds下面有詳細計算。
1.2 列表內部編碼
encoding | 含義 |
---|---|
OBJ_ENCODING_ZIPLIST | 壓縮列表 |
OBJ_ENCODING_QUICKLIST | 快表 |
新版的redis中不再使用OBJ_ENCODING_LINKEDLIST
,即雙端鏈表,有點搞不懂,註釋:
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
QUICKLIST本身是對ZIPLIST的一種封裝,ZIPLIST之前沒有介紹,可以閱讀《redis設計與實現》。
1.3 有序集合內部編碼
encoding | 含義 |
---|---|
OBJ_ENCODING_ZIPLIST | 壓縮列表 |
OBJ_ENCODING_SKIPLIST | 跳錶 |
1.4 集合內部編碼
encoding | 含義 |
---|---|
OBJ_ENCODING_INTSET | 整數集合 |
OBJ_ENCODING_HT | 哈希表 |
1.5 哈希表內部編碼
encoding | 含義 |
---|---|
OBJ_ENCODING_ZIPLIST | 壓縮列表 |
OBJ_ENCODING_HT | 哈希表 |
1.6 對象結構體
根據上面的分析,對象的類型有五種,內編碼又各不相同,給定一個對象,如何知道對象的類型以及其內部編碼呢?畢竟在對 對象進行操作時肯定要根據相應的類型調用相關函數。
爲了解決上面的問題,對象的結構體中保存了類型以及內部編碼,結構體在server.h中:
typedef struct redisObject {
unsigned type:4; //對象類型4bit
unsigned encoding:4; //編碼方式4bit
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;
對象的類型就是以下五種:
/* 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. */
編碼方式:
#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 */
引用計數與C++相似,對象被創建時引用計數爲1,引用計數爲0時對象被銷燬。
有了對象結構體,在使用命令操作對象時,就比較清晰了:
當執行一個處理數據類型的命令時,Redis 執行以下步驟:
- 根據給定key ,在數據庫字典中查找和它像對應的redisObject ,如果沒找到,就返回NULL 。
- 檢查redisObject 的type 屬性和執行命令所需的類型是否相符,如果不相符,返回類型錯誤。
- 根據redisObject 的encoding 屬性所指定的編碼,選擇合適的操作函數來處理底層的數據結構。
- 返回數據結構的操作結果作爲命令的返回值。
2. 對象操作
2.1 創建對象
由於對象有多種,創建對象的函數也有多個。
2.1.1 創建基本對象createObject
這個函數基本上是用來被其他創建對象的函數調用,type表示類型,編碼默認爲OBJ_ENCODING_RAW。
robj *createObject(int type, void *ptr) { //創建一個對象,傳入對象類型以及指向內部實現的指針ptr
robj *o = zmalloc(sizeof(*o));
o->type = type;//設置對象的類型
o->encoding = OBJ_ENCODING_RAW;//將編碼設置默認的raw,創建相應的對象時會對該字段更改
o->ptr = ptr;//指向內部實現
o->refcount = 1;//引用計數爲1
/* Set the LRU to the current lruclock (minutes resolution), or
* alternatively the LFU counter. */
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
} else {
o->lru = LRU_CLOCK();
}
return o;
}
2.1.2 創建字符串對象
字符串的底層實現有整數、sds、EMBSTR,所以創建字符串的函數也與這三種相關。
robj *createRawStringObject(const char *ptr, size_t len) { //創建sds
return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}
robj *createEmbeddedStringObject(const char *ptr, size_t len) { //創建EmbeddedString
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
struct sdshdr8 *sh = (void*)(o+1);
o->type = OBJ_STRING;//類型
o->encoding = OBJ_ENCODING_EMBSTR;//編碼
o->ptr = sh+1;
o->refcount = 1;
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
} else {
o->lru = LRU_CLOCK();
}
//初始化內部實現對象
sh->len = len;
sh->alloc = len;
sh->flags = SDS_TYPE_8;
if (ptr == SDS_NOINIT)
sh->buf[len] = '\0';
else if (ptr) {
memcpy(sh->buf,ptr,len);
sh->buf[len] = '\0';
} else {
memset(sh->buf,0,len+1);
}
return o;
}
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) //小於等於44字節的使用EMBSTR,44是由64-sizeof(redisObject)-3(sds8中的3個字節)-1('\0')得來的
return createEmbeddedStringObject(ptr,len); //sizeof(redisObject)=16,結構體對齊
else
return createRawStringObject(ptr,len); //大於44字節的使用sds
}
createStringObject
函數內部通過對字符串長度的判斷,選擇兩種不同的編碼方式,當字符串的長度小於等於44字節(不包含尾部的'\0'
)時採用EMBSTR
,大於44字節採用RAW
。
至於爲什麼是44字節呢?我們通過結構體分析一下:
jemalloc會分配8,16,32,64等字節的內存,這裏分配內存時就是64bit。
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4; //對象類型4bit
unsigned encoding:4; //編碼方式4bit
unsigned lru:LRU_BITS; //24bit
int refcount;//引用計數
void *ptr;//指向實際值的指針
} robj;
redisObject
的大小是16字節(考慮到對齊),ptr
指向的內容是一個sdshdr8
的結構體:
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
len
佔1字節,alloc
佔1字節,flags
佔1字節,buf
中的內容44字節,尾部的'\0'
佔1字節,總共佔48字節。
加上redisObject
的16字節,正好64字節,這就是44的來由。
字符串編碼中還有一個沒有講到,那就是OBJ_ENCODING_INT
,對於小整數,一般採用這種編碼方式。
robj *createStringObjectFromLongLongWithOptions(long long value, int valueobj) {
robj *o;
if (server.maxmemory == 0 ||
!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS))
{
/* If the maxmemory policy permits, we can still return shared integers
* even if valueobj is true. */
valueobj = 0;
}
if (value >= 0 && value < OBJ_SHARED_INTEGERS && valueobj == 0) { //value的值在共享的範圍內而且valueobj爲0,可以返回一個共享的對象
incrRefCount(shared.integers[value]);
o = shared.integers[value];
} else { //不能使用共享的對象則只好重新分配對象
if (value >= LONG_MIN && value <= LONG_MAX) { //如果value可以使用long類型表示,則使用OBJ_ENCODING_INT編碼
o = createObject(OBJ_STRING, NULL);
o->encoding = OBJ_ENCODING_INT;
o->ptr = (void*)((long)value);
} else {
o = createObject(OBJ_STRING,sdsfromlonglong(value));//超過long的表示範圍則將數組轉換爲字符串
}
}
return o;
}
robj *createStringObjectFromLongLong(long long value) {
return createStringObjectFromLongLongWithOptions(value,0);//如果可以,儘量創建共享對象
}
robj *createStringObjectFromLongLongForValue(long long value) {
return createStringObjectFromLongLongWithOptions(value,1);//不使用共享對象
}
這裏需要注意共享對象的概念。
有一些對象在Redis 中非常常見,比如命令的返回值OK 、ERROR 、WRONGTYPE
等字符,另外,一些小範圍的整數,比如個位、十位、百位的整數都非常常見。 爲了利用這種常見情況,Redis 在內部使用了一個Flyweight
模式:通過預分配一些常見的值 對象,並在多個數據結構之間共享這些對象,程序避免了重複分配的麻煩,也節約了一些CPU 時間。 Redis
預分配的值對象有如下這些: • 各種命令的返回值,比如執行成功時返回的OK ,執行錯誤時返回的ERROR ,類型錯誤時
返回的WRONGTYPE ,命令入隊事務時返回的QUEUED ,等等。 • 包括0 在內,
小於redis.h/REDIS_SHARED_INTEGERS 的所有整數 (REDIS_SHARED_INTEGERS
的默認值爲10000)
2.1.3 複製字符串對象
複製返回的對象引用計數總是1。
robj *dupStringObject(const robj *o) {
robj *d;
serverAssert(o->type == OBJ_STRING);//不是字符串對象會報錯
//根據不同的編碼類型創建新的對象
switch(o->encoding) {
case OBJ_ENCODING_RAW:
return createRawStringObject(o->ptr,sdslen(o->ptr));
case OBJ_ENCODING_EMBSTR:
return createEmbeddedStringObject(o->ptr,sdslen(o->ptr));
case OBJ_ENCODING_INT:
d = createObject(OBJ_STRING, NULL);//對於整數對象,創建出來的總是非共享的
d->encoding = OBJ_ENCODING_INT;
d->ptr = o->ptr;
return d;
default:
serverPanic("Wrong encoding.");
break;
}
}
2.1.4 創建列表對象
robj *createQuicklistObject(void) { //創建內部編碼爲快表的列表對象
quicklist *l = quicklistCreate();
robj *o = createObject(OBJ_LIST,l);
o->encoding = OBJ_ENCODING_QUICKLIST;//編碼爲OBJ_ENCODING_QUICKLIST
return o;
}
robj *createZiplistObject(void) { //創建內部編碼爲壓縮列表的列表對象
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_LIST,zl);
o->encoding = OBJ_ENCODING_ZIPLIST;//編碼爲OBJ_ENCODING_ZIPLIST
return o;
}
兩種編碼方式,但是在釋放列表對象的時候,如果編碼方式不是OBJ_ENCODING_QUICKLIST將會報錯,那麼是否說明OBJ_ENCODING_ZIPLIST不能用呢?(不清楚)
2.1.5 創建set對象
robj *createSetObject(void) { //創建內部編碼爲哈希表的set對象
dict *d = dictCreate(&setDictType,NULL);
robj *o = createObject(OBJ_SET,d);
o->encoding = OBJ_ENCODING_HT;
return o;
}
robj *createIntsetObject(void) { //創建內部編碼爲intset的set對象
intset *is = intsetNew();
robj *o = createObject(OBJ_SET,is);
o->encoding = OBJ_ENCODING_INTSET;
return o;
}
2.1.6 創建哈希對象
robj *createHashObject(void) { //創建內部編碼爲壓縮列表的哈希對象
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_HASH, zl);
o->encoding = OBJ_ENCODING_ZIPLIST;
return o;
}
2.1.7 創建有序集合對象
robj *createZsetObject(void) { //創建內部編碼爲跳錶的有序集合對象
zset *zs = zmalloc(sizeof(*zs));
robj *o;
zs->dict = dictCreate(&zsetDictType,NULL);
zs->zsl = zslCreate();
o = createObject(OBJ_ZSET,zs);
o->encoding = OBJ_ENCODING_SKIPLIST;
return o;
}
robj *createZsetZiplistObject(void) { //創建內部編碼爲壓縮列表的有序集合對象
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_ZSET,zl);
o->encoding = OBJ_ENCODING_ZIPLIST;
return o;
}
2.2 對象的釋放
void freeStringObject(robj *o) { //釋放字符串對象
if (o->encoding == OBJ_ENCODING_RAW) {
sdsfree(o->ptr);
}
}
void freeListObject(robj *o) { //釋放列表對象
if (o->encoding == OBJ_ENCODING_QUICKLIST) {
quicklistRelease(o->ptr);
} else {
serverPanic("Unknown list encoding type");//編碼不是OBJ_ENCODING_QUICKLIST都報錯,OBJ_ENCODING_ZIPLIST呢?
}
}
void freeSetObject(robj *o) { //釋放集合對象
switch (o->encoding) {
case OBJ_ENCODING_HT: //哈希表
dictRelease((dict*) o->ptr);
break;
case OBJ_ENCODING_INTSET://整數集合
zfree(o->ptr);
break;
default:
serverPanic("Unknown set encoding type");
}
}
void freeZsetObject(robj *o) { //釋放有序集合對象
zset *zs;
switch (o->encoding) {
case OBJ_ENCODING_SKIPLIST://跳錶
zs = o->ptr;
dictRelease(zs->dict);
zslFree(zs->zsl);
zfree(zs);
break;
case OBJ_ENCODING_ZIPLIST://壓縮列表
zfree(o->ptr);
break;
default:
serverPanic("Unknown sorted set encoding");
}
}
void freeHashObject(robj *o) { //釋放哈希對象
switch (o->encoding) {
case OBJ_ENCODING_HT: //哈希表
dictRelease((dict*) o->ptr);
break;
case OBJ_ENCODING_ZIPLIST://壓縮列表
zfree(o->ptr);
break;
default:
serverPanic("Unknown hash encoding type");
break;
}
}
2.3 引用計數的增加與減少
void incrRefCount(robj *o) { //增加引用計數
if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount++;
}
void decrRefCount(robj *o) { //減少引用計數,如果引用計數爲1,再減少變爲0,則會釋放對象
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--;
}
}
還有一些與省內存相關的函數,不再列舉。