熟悉redis的同学都应该知道,redis中主要的数据结构包括简单动态字符串sds、双端链表adlist、跳跃表skiplist、压缩列表ziplist和整数集合intset等,我们之前只分析了sds,剩下的几个数据结构我会在后面给大家一一介绍。
我们知道,redis中有字符串对象(string)、列表对象(list)、哈希对象(hash)、集合对象(set)和有序集合对象(zset),而这五种对象都至少用了上面至少一种数据结构实现。通过使用对象“接口”(暂且理解),针对不同的使用场景,对象可以使用不同的数据结构实现,这样可以优化在不同场景下的效率。下面就让我们一起来看看redis中的对象是怎么实现的:
typedef struct redisObject {
unsigned type:4; // 类型
unsigned encoding:4; // 编码
unsigned lru:REDIS_LRU_BITS; // 对象最后一次被访问的时间
int refcount; // 引用计数
void *ptr; //ptr指向实现对象的数据结构
} robj;
数据结构中type属性记录了对象的类型,分别是string、hash、list、set和zset,type占用4 bit,其取值和类型如下:
/* Object types */
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
ptr指针指向对象实现的底层数据结构,而这些数据结构由encoding属性标识,encoding也占用4 bit,其取值和对应类型如下:
#define REDIS_ENCODING_RAW 0 // Raw representation
#define REDIS_ENCODING_INT 1 // Encoded as integer
#define REDIS_ENCODING_HT 2 // Encoded as hash table
#define REDIS_ENCODING_ZIPMAP 3 // Encoded as zipmap
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
我阅读的源码是redis3.0的,zipmap已经不再使用了,我们就不会再讨论它,有兴趣的同学可以自己看看。
lru属性(占24 bit)表示对象最后一次别访问的时间,根据lru判断对象是否应该被释放,暂不做深入分析;
C语言并不具备内存回收机制,所以redis通过refcount记录robj共享的次数,当refcount为0是,表示该对象应该被释放,回收内存;
1、为5种不同的数据类型提供了统一的表达方式–类似于一个统一的接口;
2、通过使用redis对象,针对不同的使用场景,同一种数据类型可以使用不同的数据结构实现,即提升了Redis的灵活性,又可以优化对象在某一场景下的效率;
3、采用引用计数和对象共享机制,节约内存。
对象的函数主要集中在redis.h和object.c两个文件中,任何对象的创建都会调用一个底层函数–createObject()
robj *createObject(int type, void *ptr) {
robj *o = zmalloc(sizeof(*o));
o->type = type;
o->encoding = REDIS_ENCODING_RAW;
o->ptr = ptr;
o->refcount = 1;
/* Set the LRU to the current lruclock (minutes resolution). */
o->lru = LRU_CLOCK();
return o;
}
例如创建一个REDIS_ENCODING_RAW编码的string对象,
robj *createRawStringObject(char *ptr, size_t len) {
return createObject(REDIS_STRING,sdsnewlen(ptr,len));
}
createRawStringObject()就是调用了createObject()函数创建了一个string对象,其中ptr指针指向了底层的sds数据结构。
创建其他的对象类似。下面列出的是创建5种类型对象的函数:
robj *createRawStringObject(char *ptr, size_t len) //创建一个简单动态字符串编码(sds)的字符串对象
robj *createEmbeddedStringObject(char *ptr, size_t len) //创建一个embstr编码的字符串对象
robj *createStringObjectFromLongLong(long long value) //根据传入的 long long 整型值,创建一个字符串对象
robj *createStringObjectFromLongDouble(long double value) //根据传入的 long double 类型的数值,创建一个字符串对象
robj *createListObject(void) //创建一个 linkedlist 编码的列表对象
robj *createZiplistObject(void) //创建一个 ziplist 编码的列表对象
robj *createSetObject(void) //创建一个 dict 编码的集合对象
robj *createIntsetObject(void) //创建一个 intset 编码的集合对象
robj *createHashObject(void) //创建一个 ziplist 编码的哈希对象
robj *createZsetObject(void) //创建一个 dist 和 skiplist编码的有序集合对象
robj *createZsetZiplistObject(void) //创建一个 ziplist 编码的有序集合对象
我们知道,对象有引用计数机制,当obj.refcount为0时,redis就会将该对象所占的内存释放。
/*对象的引用计数增一*/
void incrRefCount(robj *o) {
o->refcount++;
}
/*
* 为对象的引用计数减一
* 当对象的引用计数降为 0 时,释放对象。
*/
void decrRefCount(robj *o) {
if (o->refcount <= 0) redisPanic("decrRefCount against refcount <= 0");
if (o->refcount == 1) {
switch(o->type) {
case REDIS_STRING: freeStringObject(o); break;
case REDIS_LIST: freeListObject(o); break;
case REDIS_SET: freeSetObject(o); break;
case REDIS_ZSET: freeZsetObject(o); break;
case REDIS_HASH: freeHashObject(o); break;
default: redisPanic("Unknown object type"); break;
}
//释放对象
zfree(o);
// 减少计数
} else {
o->refcount--;
}
}
从代码可以看出,释放对象时,现根据对象的类型type,释放对象保存的数据结构,再释放对象。例如释放列表对象时:
void freeListObject(robj *o) {
//根据不同的编码实现,调用不同的底层释放函数
switch (o->encoding) {
case REDIS_ENCODING_LINKEDLIST:
listRelease((list*) o->ptr);
break;
case REDIS_ENCODING_ZIPLIST:
zfree(o->ptr);
break;
default:
redisPanic("Unknown list encoding type");
}
}
对象释放函数:
void freeStringObject(robj *o) //释放字符串对象
void freeListObject(robj *o) //释放列表对象
void freeSetObject(robj *o) //释放集合对象
void freeZsetObject(robj *o) //释放有序集合对象
void freeHashObject(robj *o) //释放哈希对象
其他对象的操作函数都在object.c文件中,这里就不一一列举了,不过有些函数可能之后我们会讲到,例如tryObjectEncoding()等。
其实,redis为我们提供了object命令,我们可以根据key获取redis内部对象的一些参数:
函数的具体实现:
void objectCommand(redisClient *c) {
robj *o;
//返回key所指对象的引用计数
if (!strcasecmp(c->argv[1]->ptr,"refcount") && c->argc == 3) {
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk)) == NULL) return;
addReplyLongLong(c,o->refcount);
// 返回key所指对象的编码
} else if (!strcasecmp(c->argv[1]->ptr,"encoding") && c->argc == 3) {
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))== NULL) return;
addReplyBulkCString(c,strEncoding(o->encoding));
//返回key所指对象的空闲时间
} else if (!strcasecmp(c->argv[1]->ptr,"idletime") && c->argc == 3) {
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))== NULL) return;
addReplyLongLong(c,estimateObjectIdleTime(o)/1000);
} else {
addReplyError(c,"Syntax error. Try OBJECT (refcount|encoding|idletime)");
}
}
好吧,就分析到这~加油