西大 发表于 2016-12-19 09:48:43

redis源码阅读笔记(7)——对象

本篇我们研究redis里的对象。
1. 概述
redis有5种对象类型

[*]字符串
[*]列表
[*]哈希
[*]集合
[*]有序集合

它们的底层数据结构都是由前面6篇文章提到的所实现的。
请先参考如下文章,并结合源码阅读体会。
http://www.redisbook.com/en/latest/toc.html里的8.对象
http://origin.redisbook.com/en/latest/里的第三部分:Redis 数据类型
2. 一种类型,多种实现
redis使得一种对象类型的实现可以由不同的数据结构来切换,从而同时兼顾性能(CPU)和内存。而且当某个底层数据结构有性能问题时,可以替换掉。当然这种替换的实现还是没有java这么优雅的,redis里面只能靠if else来切换不同的数据结构。
对象类型实现方式一实现方式二实现方式三字符串REDIS_ENCODING_INTREDIS_ENCODING_EMBSTRREDIS_ENCODING_RAW列表REDIS_ENCODING_ZIPLISTREDIS_ENCODING_LINKEDLIST哈希REDIS_ENCODING_ZIPLISTREDIS_ENCODING_HT集合REDIS_ENCODING_INTSETREDIS_ENCODING_HT有序集合REDIS_ENCODING_ZIPLISTREDIS_ENCODING_SKIPLIST
3. zipmap
以前小的哈希表使用zipmap实现的,而redis从2.6版本开始,改用ziplist来实现,因为zipmap被爆有性能问题。
所以现在zipmap已经没用了,如果读者感兴趣的话,代码就在zipmap.c里面。我就不关注了。
4. redisObject的定义
redis.h中定义了redisObject

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;


这里解释一下里面的一个奇怪的语法:(冒号)
该语法的定义叫位字段(bit field)。
4.1 位字段(bit field)
type:4的意思是定义了一个属性叫type,占4个比特,而前面的unsigned等价于unsigned int。
下面又定义了一个属性叫encoding,占4个比特。以及lru,占24个比特。
这样这3个属性合在一起,占32个比特,也就是4个字节。
剩下的refcount和ptr自不必说,各占4个字节。
所以整个结构体robj占了12个字节。可以看到巧妙运用了bit field以后,可以节省内存,同时相对直接操纵位来编程也更容易。
4.2 写个程序测试一下
bitfieldtest.c(可在附件下载)

int main(void)
{
printf("sizeof(robj)=%d\n", sizeof(robj));
robj obj1;
void *address = &obj1;
printf("address=%#X\n", address);
obj1.type = 1;
obj1.encoding = 5;
obj1.lru = 255;
obj1.refcount = 2;
obj1.ptr = NULL;
return 0;
}


在我的32位的win7下测试结果和预期一致。

sizeof(robj)=12
address=0X22ABE0


我们随后对结构体robj的属性赋了值,用eclipse调试观察内存如下图所示。

图已经很明白了,文字就不再赘述了,只解释一点,type和encoding为何顺序是倒过来的,这个和大端小端有关系。
5. 对象共享
用flyweight模式,将一些常用的整数对象缓存起来。这个思路和java里面的Integer.valueOf如出一辙。
redis.h中定义

struct sharedObjectsStruct {
//......此处省略一部分代码......
*integers;
};


redis.c中初始化

#define REDIS_SHARED_INTEGERS 10000
struct sharedObjectsStruct shared;
void createSharedObjects(void) {
int j;
//......此处省略一部分代码......
// 常用整数
for (j = 0; j < REDIS_SHARED_INTEGERS; j++) {
shared.integers = createObject(REDIS_STRING,(void*)(long)j);
shared.integers->encoding = REDIS_ENCODING_INT;
}
}


可以看到将0~9999的整数缓存了起来。而java里面的Integer则是将-128到127缓存了起来。
6. 引用计数以及对象的销毁
引用计数是垃圾回收的一种算法,但是此算法无法解决循环引用问题,所以java里面没有采用这种算法。

/*
* 为对象的引用计数减一
*
* 当对象的引用计数降为 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--;
}
}
/*
* 释放字符串对象
*/
void freeStringObject(robj *o) {
if (o->encoding == REDIS_ENCODING_RAW) {
sdsfree(o->ptr);
}
}


在这里我不禁要感叹c程序员的功力了,事无巨细都亲力亲为。而java程序员就幸福多了,语言,类库还有框架的封装使得程序员生产力大增,且不用动用脑细胞做这类琐事。
7. 具体对象类
下面稍微记录一下redis中5种对象类型所对应的源码位置,方便读者自学。
7.1 字符串对象
redis.h中定义了原型

/* Redis object implementation */
robj *createObject(int type, void *ptr);
robj *createStringObject(char *ptr, size_t len);
robj *createRawStringObject(char *ptr, size_t len);
robj *createEmbeddedStringObject(char *ptr, size_t len);
robj *createStringObjectFromLongLong(long long value);
robj *createStringObjectFromLongDouble(long double value);


实现在object.c里
会分3情况创建不同的实现。
7.2 列表对象
见t_list.c
举例:
rpush命令键入后将执行的函数是rpushCommand-->pushGenericCommand-->listTypePush
listTypePush函数里可以看到,如果是ziplist,则调用ziplistPush,如果是adlist,则调用listAddNodeHead或listAddNodeTail
具体的每个命令所调用的函数,可参考
http://www.redisbook.com/en/latest/preview/object/list.html#id3
阻塞。比较难懂,看不懂可以放到以后再看。
http://origin.redisbook.com/en/latest/datatype/list.html#id4
7.3 哈希对象
见t_hash.c
举例:
hset命令键入后将执行的函数是hsetCommand-->hashTypeSet
hashTypeSet函数里可以看到,如果是ziplist,则调用ziplistPush两次,将key和value都加入列表尾部,如果是hashtable(dict),则调用dictReplace
具体的每个命令所调用的函数,可参考
http://www.redisbook.com/en/latest/preview/object/hash.html#id3
7.4 集合对象
见t_set.c
举例:
sadd命令键入后将执行的函数是saddCommand-->setTypeAdd
setTypeAdd函数里可以看到,如果是hashtable(dict),则调用dictAdd,如果是intset,则调用intsetAdd
具体的每个命令所调用的函数,可参考
http://www.redisbook.com/en/latest/preview/object/set.html#id3
7.5 有序集合对象
见t_zset.c
为了让有序集合的查找和范围型操作都尽可能快地执行, Redis 选择了同时使用字典和跳跃表两种数据结构来实现有序集合。

typedef struct zset {
// 字典,使得ZSCORE只需要O(1)
dict *dict;
// 跳跃表,使得ZRANK只需要O(1)
zskiplist *zsl;
} zset;


举例:
zadd命令键入后将执行的函数是zaddCommand-->zaddGenericCommand
zaddGenericCommand函数里可以看到,如果是ziplist,则调用zzlInsert,进而调用ziplistInsert两次,将value和score都加入列表,如果是skiplist,则调用zslInsert插入跳跃表,然后调用dictAdd插入到字典。
具体的每个命令所调用的函数,可参考
http://www.redisbook.com/en/latest/preview/object/sorted_set.html#id3
页: [1]
查看完整版本: redis源码阅读笔记(7)——对象