Redis源码剖析——对象

这篇文章将剖析Redis提供给用户的5种对象底层的数据结构和接口

前面讲了Reids底层所使用的一些数据结构:SDS、双端链表、字典、跳跃表等等,但是Redis并没有直接使用这些数据结构来构造键值对数据库。

对象数据结构

在redis.h中定义类redisObject的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
// Redis 对象定义
typedef struct redisObject {
unsigned type:4; // 类型
unsigned encoding:4; // 编码
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 实现的数据结构
void *ptr;
} robj;

从代码中我们可以看到Redis对象所包含的信息有:类型、编码、实现指针、引用计数和访问时间

类型

对于 Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象(string)、列表对象(list)、哈希对象(hash)、集合对象(set)或者有序集合对象(zset)的其中一种

1
2
3
4
5
6
/* 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属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现

1
2
3
4
5
6
7
8
9
#define REDIS_ENCODING_RAW 0 /* 编码为 简单动态字符串 */
#define REDIS_ENCODING_INT 1 /* 编码为 long类型的整数 */
#define REDIS_ENCODING_HT 2 /* 编码为 字典 */
#define REDIS_ENCODING_ZIPMAP 3
#define REDIS_ENCODING_LINKEDLIST 4 /* 编码为 双端链表 */
#define REDIS_ENCODING_ZIPLIST 5 /* 编码为 压缩列表 */
#define REDIS_ENCODING_INTSET 6 /* 编码为 整数集合 */
#define REDIS_ENCODING_SKIPLIST 7 /* 编码为 跳跃表和字典 */
#define REDIS_ENCODING_EMBSTR 8 /* 编码为 embstr编码的简单动态字符串 */

每种类型的对象都至少对应两种不同的编码

引用计数

Redis的对象系统实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外, Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同个对象来节约内存。

上面这些功能都是通过refcount这个属性来实现的:

  • 在创建一个新对象时,引用计数的值会被初始化为1
  • 当对象被一个新程序使用时,它的引用计数值会被增一
  • 当对象不再被一个程序使用时,它的引用计数值会被减一
  • 当对象的引用计数值变为0时,对象所占用的内存会被释放

修改引用计数的api如下:

1
2
3
void decrRefCount(robj *o);
void incrRefCount(robj *o);
robj *resetRefCount(robj *obj);

内存回收

我们可以通过decrRefCount的实现看到当引用计数到达0的时候会自动释放对象所占有的资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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--;
}
}

对象共享

如果想创建一个与另外一个对象含有相同值的对象,这个时候可以启动对象的共享机制。

在Redis中,让多个键共享同一个值对象需要执行以下两个步骤

  • 将数据库键的值指针指向一个现有的值对象
  • 将被共享的值对象的引用计数增一

比如在从LongLong创建一个字符串对象的时候,首先要判断在不在共享对象的范围内,如果在的话就对引用计数加1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
robj *createStringObjectFromLongLong(long long value) {
robj *o;
// value 的大小符合 REDIS 共享整数的范围
// 那么返回一个共享对象
if (value >= 0 && value < REDIS_SHARED_INTEGERS) {
incrRefCount(shared.integers[value]);
o = shared.integers[value];
// 不符合共享范围,创建一个新的整数对象
} else {
// 值可以用 long 类型保存,
// 创建一个 REDIS_ENCODING_INT 编码的字符串对象
if (value >= LONG_MIN && value <= LONG_MAX) {
o = createObject(REDIS_STRING, NULL);
o->encoding = REDIS_ENCODING_INT;
o->ptr = (void*)((long)value);
// 值不能用 long 类型保存(long long 类型),将值转换为字符串,
// 并创建一个 REDIS_ENCODING_RAW 的字符串对象来保存值
} else {
o = createObject(REDIS_STRING,sdsfromlonglong(value));
}
}
return o;
}

对象的基本操作

Redis对象的基本操作包含了创建对象、销毁对象、编码转换等等

大部分操作的实现都在object.c文件中

对象创建

各种对象创建的API如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
robj *createObject(int type, void *ptr); // 创建对象,设定参数
robj *createStringObject(char *ptr, size_t len); // 创建字符串对象
robj *createRawStringObject(char *ptr, size_t len); // 创建raw编码的字符串对象
robj *createEmbeddedStringObject(char *ptr, size_t len); // 创建embstr编码的字符串对象
robj *createStringObjectFromLongLong(long long value); // 根据传入LongLong创建字符串对象
robj *createStringObjectFromLongDouble(long double value); // 根据传入的LongDouble创建字符串对象
robj *createListObject(void); // 创建双端链表链表编码的列表对象
robj *createZiplistObject(void); // 创建压缩列表编码的列表对象
robj *createSetObject(void); // 创建集合对象
robj *createIntsetObject(void); // 创建整型集合编码的集合对象
robj *createHashObject(void); // 创建hash对象
robj *createZsetObject(void); // 创建zset对象
robj *createZsetZiplistObject(void); //创建压缩列表编码的zset对象

具体我们以列表对象为例,看看创建对象的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 创建一个 LINKEDLIST 编码的列表对象
*/
robj *createListObject(void) {
list *l = listCreate();
robj *o = createObject(REDIS_LIST,l);
listSetFreeMethod(l,decrRefCountVoid);
o->encoding = REDIS_ENCODING_LINKEDLIST;
return o;
}
/*
* 创建一个 ZIPLIST 编码的列表对象
*/
robj *createZiplistObject(void) {
unsigned char *zl = ziplistNew();
robj *o = createObject(REDIS_LIST,zl);
o->encoding = REDIS_ENCODING_ZIPLIST;
return o;
}

对象销毁

各种对象销毁的API如下:

1
2
3
4
5
void freeStringObject(robj *o);
void freeListObject(robj *o);
void freeSetObject(robj *o);
void freeZsetObject(robj *o);
void freeHashObject(robj *o);

以List为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* 释放列表对象
*/
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 listRelease(list *list)
{
unsigned long len;
listNode *current, *next;
// 指向头指针
current = list->head;
// 遍历整个链表
len = list->len;
while(len--) {
next = current->next;
// 如果有设置值释放函数,那么调用它
if (list->free) list->free(current->value);
// 释放节点结构
zfree(current);
current = next;
}
// 释放链表结构
zfree(list);
}

和创建对象分多钟编码格式相对应,释放对象的时候也要根据编码具体执行释放

对象交互指令

Redis提供三个命令用户获取对象的一些参数:

  • object refcount 返回key所指的对象的引用计数
  • object encoding 返回key所指的对象中存放的数据的编码方式
  • object idletime 返回key所指的对象的空转时长

具体的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void objectCommand(redisClient *c) {
robj *o;
// 返回对戏哪个的引用计数
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);
// 返回对象的编码
} 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));
// 返回对象的空闲时间
} 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)");
}
}