Redis源码解析3

来源:互联网 发布:软件使用培训怎么说 编辑:程序博客网 时间:2024/06/01 23:44

Redis源码解析3 - Object

http://www.cnblogs.com/curve/archive/2012/09/18/2686900.html

 (转载请注明出处: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 handle10 } 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,就行了。