Redis源码解析3 - Object
Everything is Object
(转载请注明出处:http://www.cnblogs.com/curve/archive/2012/09/18/2686900.html)
数据结构
在Redis中,用 robj 结构表示一切数据对象,可以把它看作一种元数据(MetaData)
各种不同的结构化数据,通过该对象进行封装、传递、变换、编码,而该对象本身却十分简单
其类型定义如下:
1 typedef struct redisObject { 2 unsigned type:4; // 数据类型 3 unsigned storage:2; // 数据存储位置(VM管理中用到) 4 unsigned encoding:4; // 数据编码方式 5 unsigned lru:22; //lru时间计数 6 int refcount; // 引用计数 7 } robj;
type字段表示数据类型,有以下几种定义:
REDIS_STRING // 字符串
REDIS_LIST // 链表
REDIS_SET // 集合
REDIS_ZSET // 有序集合
REDIS_HASH // HASH结构(注意,此处不同于传统意义上的哈希表(如stl::hash_map),这里的hash仅有字段散列的语义)
REDIS_VMPOINTER // VM指针(表示数据处于VM管理之下)
encoding字段表示数据编码方式,有以下几种定义:
REDIS_ENCODING_RAW // 原始编码,就是一个原始字符串
REDIS_ENCODING_INT // INT型编码,会将数字类型的字符串编码成该格式
REDIS_ENCODING_HT // 哈希表编码,源代码中以dict结构来管理
REDIS_ENCODING_ZIPMAP // 精简编码的hash结构,更省内存
REDIS_ENCODING_LINKEDLIST // 双向链表
REDIS_ENCODING_ZIPLIST // 精简编码的链表,更省内存
REDIS_ENCODING_INTSET // 精简编码的集合,更省内存
REDIS_ENCODING_SKIPLIST //
refcount字段是对象引用计数,每多一次引用,该计数加1;每减少一次引用,该计数减1,若减至0,则释放该对象内存
操作流程
Redis源代码中,有大量的robj指针在函数间传递
下面,以redis处理“set”命令为例,对 robj的操作进行讲解
1. 在 ProcessInlineBuffer、ProcessMultiBulkBUffer中,对命令缓存进行解析
通常是以空格为分隔符进行分离,每一个字符块都编码为一个独立的robj对象
对象存储在redisClient结构中的argc数组中,提供给后续函数使用
InlineBuffer:对应于Redis单行命令
MultiBulkBuffer:对应于多行命令
2. ProcessCommand对命令进行解析,然后进行函数分发
处理流程进入具体的命令处理函数
3. setCommand是对应于“set”命令的处理函数
该函数非常简单,主要起到一个命令接入作用
它会对obj-val进行一次尝试性的编码转换,在本例中,会尝试将val对象转换为一个INT型的对象
转换完成后,进入内部共享函数setGenericCommand处理流程
4. setGenericCommand进行实际的“set”操作逻辑处理,即:
将kv键值对,加入到该连接对应的命名空间中(即一个dict结构)
对应于该dict结构,插入操作的具体语义由一个全局性的 dictType 进行定义:
1 dictType commandTableDictType = { 2 dictSdsCaseHash, // hashid计算函数 3 NULL, // key构造函数,在本例中NULL表示直接指针赋值 4 NULL, // val构造函数,在本例中NULL表示直接指针赋值 5 dictSdsKeyCompare, // key比较函数 6 dictSdsDestructor, // key析构函数,该函数会将key作为一个原始字符串进行内存释放 7 dictRedisObjectDestructor // value析构函数,会对obj-val做一次decrRefCount操作 8 };
根据这个操作定义,执行完比后
obj-key会复制一份原始字符串,将指针加入dict中(复制操作在dbAdd函数中实现)
obj-val直接将指针加入dict中,同时将该对象的refcount加1(加引用操作在setKey函数中实现)
5. 整个处理流程结束后,释放redisClient中的argc对象数组
在本例中,导致的结果是:
obj-key引用计数减1,最终值为0,导致对象删除
obj-val引用计数减1,最终值为1,该对象继续存在于全局key的dict表中
继续上一条“set”命令,针对更进一步的命令,对redis的对象编码转换作个初步了解
在下图中,主要注意 appendCommand中,由于该命令需要改变 obj-val对象的值,从而导致obj-val从INT编码状态解码到RAW编码状态。
解码时生成了临时对象 obj-decoded
临时对象与obj-append合并,将合并后的值赋给obj-val
引用计数
php中的变量
通过引用计数管理对象的生存期,是个好主意。通过上面两张图,也可以初步看出,redis如何通过引用计数来管理对象的生死
在很多动态类型的语言中,也有类似的做法
比如在PHP中,用以下结构表示变量:
1 struct _zval_struct { 2 zvalue_value value; // 变量的值 3 zend_uint refcount; // 直接引用计数 4 zend_uchar type; // 变量类型 5 zend_uchar is_ref; // 间接引用计数 6 }; 7 typedef struct _zval_struct zval;
其中,value是一个 “zvalue_value” 类型的指针
zvalue_value是一个union类型的结构,将不同类型的数据整合进了同一个结构里
1 typedef union _zvalue_value { 2 long lval; // long value 3 double dval; // double value 4 struct { 5 char *val; 6 int len; 7 } str; // string value 8 HashTable *ht; // hash table(array) value 9 zend_object_value obj; // resource handle 10 } zvalue_value;
可以看到,PHP中的变量有两种引用计数
1. refcount:直接引用计数,赋值时计数增加
2. is_ref:间接引用计数,赋引用时计数增加
之所以有这两个值,是因为PHP中的变量有引用的概念,并且涉及到几个重要原则:
1. 赋值零拷贝:变量赋值时,不会复制一份新的变量,而是直接对zval结构加引用,且引用计数加在refcount变量上
但是,自定义类对象的赋值,和普通变量不同,默认是赋引用,导致计数加在is_ref上
我认为这是PHP语言设计上很混乱的一个地方
2. 写时复制:变量改变会引发变量分离,导致复制一份新变量
3. 写时改变:变量如果加了is_ref引用,那么变量改变时,不会引发分离,而是直接改变zval的值,使数个引用的变量可以共享改变
注意,第2点和第3点是冲突的
在redis中,robj的设计在思想上同PHP的变量设计是相同的
不过robj没有赋引用的概念,所以设计上应该更简单一些
不过,robj上有encode的概念,这在PHP里是没有的,这会导致更多的编码转换
编码转换的相关话题,会在后续几篇文章中详细解释
共享对象
redis中,为了操作方便,同时为了节省内存,定义了一组全局的共享对象“shared”
1 struct sharedObjectsStruct { 2 robj *crlf, *ok, *err, *emptybulk, *czero, *cone, *cnegone, *pong, *space, 3 *colon, *nullbulk, *nullmultibulk, *queued, 4 *emptymultibulk, *wrongtypeerr, *nokeyerr, *syntaxerr, *sameobjecterr, 5 *outofrangeerr, *loadingerr, *plus, 6 *select0, *select1, *select2, *select3, *select4, 7 *select5, *select6, *select7, *select8, *select9, 8 *messagebulk, *pmessagebulk, *subscribebulk, *unsubscribebulk, *mbulk3, 9 *mbulk4, *psubscribebulk, *punsubscribebulk, 10 *integers[REDIS_SHARED_INTEGERS]; 11 }; 12 13 extern struct sharedObjectsStruct shared;
如上定义,比如其中的 crlf 代表一个“\r\n”字符串对象,ok代表一个"ok"字符串对象
值得注意的是其中有一个 integers的变量,定义为10000大小的数组
这个变量是干什么的呢?
其实,这应该算是redis里十分精巧的一个设计
因为Redis设计者认为,数字1~10000是绝大部分系统中,都会频繁使用到的数字
所以,系统预定义了这10000个INT对象,以达到节省内存的目的
以后凡是用户输入中包含有10000以内的整数
都不用重新生成新的对象了,而是直接保存一个指针,并将对应共享对象的引用计数加1,就行了。