向优秀代码学习:Redis 源码概览

这七年以来,主要是在写Java和Scala代码,我的C语言技能都退化了。事实上,它可能已经完全没有了。除了偶尔会用来hack,大学毕业以后我基本上都没有用到C了。大家都说,阅读他人的代码是非常好的学习方法,特别是代码库的作者是专家或者它质量有很高的评价时。因此,我准备阅读一个这样的代码库:Redis。

(相关阅读:《阅读优秀代码是提高开发人员修为的一种捷径》)

Redis是一个用ANSI C 编写的开源数据结构服务器。“数据结构服务器”只是对灵巧的key-value存储服务的另外一种称谓。你不仅仅可以存储简单的字符串,还可以存储包括hash(或者map,甚至dicts),list,set,sorted set。我们在Top10中大量应用了Redis,大部分为了根据用户搜索的日期和酒店的空房情况和价格建立索引。我发现Redis的代码非常容易读懂,甚至是对于像我这样的新手。代码写的很整洁,并且代码量相对较小(4.5万行左右),大部分都是单线程的,依赖也很少。所有的依赖都跟源代码放在一起了,这中做法让编译它变得非常简单:clone它的库,然后输入make即可。

我决定通过为它增加一条命令来深入代码。而这简单的事情可以让我知道Redis怎么处理一条命令并调度响应它。命令rand,接收一个整型值作为max,并随机返回0到max(不包含max)之间的一个整数。这不是使用键值存储的思路,但是实现它将会很有启发性。而我也肯定不会提交一个pull request。

免责声明:如我之前所说,我绝对不是一个C语言的专家,因此这里所有的代码和其解释都符合这个条款。而且,我链接了Redis的一个不稳定分支,所以它是不稳定的。如果你自己去获取Redis源码,用你喜欢的编辑器来查看时,你将发现更多本文的不同,特别是如果你编译并运行时会发现不同。

命令表在src/redis.c文件的靠顶部的位置。它是一个数组,数组的元素类型是redisCommand结构体。redisCommand是在src/redis.h中定义的。在redisCommandTable的上方有一块比较详细的注释,对它的每一个field做了解释。下面是get命令的定义:

{“get”,getCommand,2,”r”,0,NULL,1,1,1,0,0},

第一个field是命令的名字“get”。第二个field是一个函数指针,指向这个命令的具体实现(你可以查看实现细节t_string.c)。

第三个field是命令的参数数量限制(命令接收的参数个数)。指定这个,意味着在调用函数指针之前,查找和执行命令的代码可以做一个预先验证。这种做法减少了在每个命令函数必须的错误处理代码。参数的个数算上了命令名字本身,所以它只接受两个参数:它自己的名字,key的名字(我们要获取它的值)。

第四个field,被设为”r”,用来指明这个命令是只读的,不能修改这个key的value或状态。有一大堆的字母标志,你都可以用在这个位置。而且在附近的注释块中,每个字母标志都有详细的解释。紧跟这个field的field总是被设置为0,后面会用来计算。它只是第四个field的字符串包含信息的位掩码。

第六个field是NULL,因为它只有在你要用复杂的逻辑去告诉Redis哪个参数才是真正的key的时候才需要。一个key指向一个存储在Redis中的值的引用,对应简单的参数,例如我们的max参数。这种机制,允许Redis在调用命令的实现之前,提取key的值(并且校验key是否存在)。如果这个field被设置了值,那么它将会是一个函数指针,指向的函数会返回一个参数索引的整型数组(db.c中的zunionInterGetKeys是一个示例)。在get命令(其他大部分命令)的场景下,这个数组的信息传达的信息跟后面三个field的一样。get命令只有一个参数,而它就是key。因此,第一个参数(key)在位置1上,最后一个参数(也是key)在位置1上,从第一个参数到最后一个参数的增量也是1(译者注:源码注释是:intkeystep;/* The step between first and last key */)。

redisCommand的最后两个field是命令的度量项,由Redis来设置,并且总是初始化为0。

在命令表的底部加上我们的命令:

{“rand”,randCommand,2,”rRl”,0,NULL,0,0,0,0,0}

命令的名字是“rand”,randCommand指向实现的指针(还未实现),它接收2个参数(命令名字和max)。至于标志,它是只读的(r),返回随机的,不确定的输出(R),而且它可以在Redis还在加载数据的时候使用(l)。它没有关键参数。

下一步是在src/redis.h中增加randCommand的函数原型。Redis命令的函数接收一个参数,一个redisClient的结构体,作为命令的参数同时也用来向实际的客户端发送响应。

void randCommand(redisClient *c);

这个原型应该放在src/redis.h中与其他所有命令的原型一起。搜索下面的一行:

/* Commands prototypes */

这将帮你找到正确的位置。

我们在src/redis.c中加一个空实现:

void randCommand(redisClient *c) {

}

我将它加在了infoCommand定义的旁边。现在,我们执行make命令。

make

然后,启动我们刚刚编译成共的Redis服务(如果你已经有一个Redis服务在本地运行,你应该停掉它):

> src/redis-server

接着我们在另外的终端中运行Redis客户端,并试着运行我们的命令:

>redis-cli

首先,我们试一试我们的异常处理:

redis 127.0.0.1:6379> rand(error) ERR wrong number of arguments for ‘rand’ command

很好,参数数量限制检查是正常的。这一次我们指定一个参数:

redis 127.0.0.1:6379> rand 1

Redis卡住了。这正是我预期的,因为我在randCommand函数中没有任何响应。将服务停掉,我们接着回去看代码。

我们想返回一个整数,因此我在代码里翻找例子,最后在src/t_zset.c中找到了zcardCommand。这个命令用addReplyLongLong来向客户端返回一个64位(long long)的整数。我们也试一下:

void randCommand(redisClient *c) {addReplyLongLong(c,3);}

然后,我们在make一次,并测试命令:

redis 127.0.0.1:6379> rand 1(integer) 3

redis 127.0.0.1:6379> rand 2(integer) 3

redis 127.0.0.1:6379> rand 3(integer) 3

好吧,,结果不是太随机,但这只是个开始。我们从命令里获取参数max,并返回一个由max限制的随机数:

void randCommand(redisClient *c) {long max;

if (getLongFromObjectOrReply(c,c->argv[1],&max,NULL) != REDIS_OK)return;

addReplyLongLong(c,random() % max);}

曾经拥有的不要忘记,难以得到的更要珍惜,

向优秀代码学习:Redis 源码概览

相关文章:

你感兴趣的文章:

标签云: