Redis源码速览
发布日期:2016-4-21 12:4:45
Redis源码速览 一、Introduction 阅读成熟开源项目的代码是人们常说写代码进步最快的方式之一,我们可以从中可以学习到许多良好的代码风格、问题抽象实践。在本文我选择了下面这个代码质量被广泛认可的开源项目源码进行阅读,如图1所示: Redis是一个用c语言实现的key-value store。redis在除了最基础的基于字符串的键值对,还支持以下数据结构:
所以redis也常被称为是一个data structure server。 我使用的redis源码版本是redis 3.0.2版本。Redis的编译、运行比mssql简单。因为它将所有依赖项均以源码方式加入项目中,在代码根目录下一句简单的make就可以完成所有编译任务,再来一句make test就可以完成所有测试。从代码下载到完成测试,整个过程耗时竟然没有超过5分钟,这简直是高效。 我决定以实现一条简单命令的方式逐步阅读redis的代码来避免在茫茫代码中走神,我打算实现的命令是randget,它接受一个列表名作为参数,并随机返回列表中的一个元素。这的确是一个实际用处不大的命令,但应该能帮助我们更有目的性的阅读代码。 二、In Action 我们正式开始实现这个随机返回数组元素的 randget 命令。 Redis所支持的所有命令均存储于redis.c文件开头的 redisCommandTable 数组中,如以下所示的代码: struct redisCommand redisCommandTable[] = { {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0}, {"randget",randgetCommand,2,"rF",0, NULL,1,1,1,0,0}, {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0}, ... 数组中的每个元素是一个 redisCommand 结构体,结构体中记录了关于一条命令的详尽信息,可见于redis.h, 以数组中记录的第一条命令 get 命令为例,这个例子说的是: 命令行:{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0} 说明:
我就基于这个首先在数组中加入了这个条目: {"randget",randgetCommand,2,"rF",0, NULL,1,1,1,0,0}, 由于这是个与列表相关的命令,我决定把函数 randgetCommand 和其他列表相关的函数放在一起。首先在 redis.h 加入一行声明,代码如下所示: void randgetCommand(redisClient *c); 在 t_list.c 中加入函数定义,代码如下所示: void randgetCommand(redisClient *c){ } 我们不妨编译一下: > make 能够编译通过,那我们来试一试命令,先启动服务器端,代码如下所示: > src/redis-server 再在另外一个shell启动客户端,代码如下所示: > src/redis-cli 并敲入如下所示: 127.0.0.1:6379> LPUSH mylist foo (integer) 1 127.0.0.1:6379> randget (error) ERR wrong number of arguments for 'randget' command 127.0.0.1:6379> randget mylist [hang up] 从上面的代码可以看到redis能正确识别 randget 应接收的参数个数。当参数个数正确时,redis识别了我们敲入的命令,但由于我们还没有填入命令的实现,程序没有正确进行回应而且程序挂起了。 接下来就是在函数体中填入我们的实现了,命令需要接收一个列表名作为输入,并随机返回列表中的一个元素,抽象来说就是以下所说的代码: void randgetCommand(redisClient *c){ // 读入参数 // 使用该参数是否可取出某个列表? // 若否,返回空 // 读入列表,得到列表长度 length // 若length为0 // 返回空 // 否则 // 随机取出[0,length)中某个整数作为下标,取出对应元素 } 首先是获取命令参数并进行检查,由于各个关于列表的命令都需要使用类似的操作,我们参考了其他命令中的实现,代码如下所示:: robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk); if (o == NULL || checkType(c,o,REDIS_LIST)) return; 代码首先使用 c->argv[1] 获取列表名,并调用 lookupKeyReadOrReply 获取这个键对应的值,若给定的键不存在则向客户端返回空,且函数返回null。 而在第二行将检查 o 是否为 null 及 o 所存储的值类型是否为列表,当 o 为空或类型不为列表时,将向客户端返回空,并从这一命令中返回。 当 检查顺利通过时, o 保存的就是我们感兴趣的列表结构,我们首先获取列表的长度,若长度为0则返回空,否则随机得到一个下标并返回对应元素,代码如下所示:: if (length == 0){ addReply(c,shared.nullbulk); } else { long index = random() % length; robj *value; ... } Redis中列表的存储方式有两种,分别是 Linked List 和 Zip List, Linked List即为我们熟悉的链表,而关于 Zip List 是一种为了节省内存消耗而特别设计的列表结构,它的详细介绍可见于这篇文章。我们根据列表的不同存储方式使用相应接口获取下标为 index 的元素,代码如下所示:: if (o->encoding == REDIS_ENCODING_ZIPLIST) { unsigned char *p; unsigned char *vstr; unsigned int vlen; long long vlong; p = ziplistIndex(o->ptr,index); if (ziplistGet(p,&vstr,&vlen,&vlong)) { if (vstr) { value = createStringObject((char*)vstr,vlen); } else { value = createStringObjectFromLongLong(vlong); } addReplyBulk(c,value); decrRefCount(value); } else { addReply(c,shared.nullbulk); } } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) { listNode *ln = listIndex(o->ptr,index); if (ln != NULL) { value = listNodeValue(ln); addReplyBulk(c,value); } else { addReply(c,shared.nullbulk); } } else { redisPanic("Unknown list encoding"); } 这段代码看起来有点复杂,所完成的工作就是以获取列表中的元素,将所得元素的指针存储于 value中,并通过 addReply 或 addReplyBulk 将所得元素返回。至此命令所要完成的工作就完成了,函数的全貌如下面,代码如下所示:: void randgetCommand(redisClient *c){ robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk); if (o == NULL || checkType(c,o,REDIS_LIST)) return; long long length = listTypeLength(o); if (length == 0){ addReply(c,shared.nullbulk); } else { long index = random() % length; robj *value; if (o->encoding == REDIS_ENCODING_ZIPLIST) { unsigned char *p; unsigned char *vstr; unsigned int vlen; long long vlong; p = ziplistIndex(o->ptr,index); if (ziplistGet(p,&vstr,&vlen,&vlong)) { if (vstr) { value = createStringObject((char*)vstr,vlen); } else { value = createStringObjectFromLongLong(vlong); } addReplyBulk(c,value); decrRefCount(value); } else { addReply(c,shared.nullbulk); } } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) { listNode *ln = listIndex(o->ptr,index); if (ln != NULL) { value = listNodeValue(ln); addReplyBulk(c,value); } else { addReply(c,shared.nullbulk); } } else { redisPanic("Unknown list encoding"); } } return; } 接下来我们重新编译redis,并测试一下测试代码如下所示: [client] > src/redis-cli # 初始化列表 127.0.0.1:6379> LPUSH list a (integer) 1 127.0.0.1:6379> LPUSH list b (integer) 2 127.0.0.1:6379> LPUSH list c (integer) 3 127.0.0.1:6379> LPUSH list d (integer) 4 127.0.0.1:6379> LPUSH list e (integer) 5 # 插入完毕,开始随机获取 127.0.0.1:6379> randget list "e" 127.0.0.1:6379> randget list "a" 127.0.0.1:6379> randget list "b" 127.0.0.1:6379> randget list "d" 127.0.0.1:6379> randget list "e" 127.0.0.1:6379> randget list "d" # 错误处理 127.0.0.1:6379> set foo bar OK 127.0.0.1:6379> randget (error) ERR wrong number of arguments for 'randget' command 127.0.0.1:6379> randget foo (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379> randget unknown (nil) 127.0.0.1:6379> 可以看到命令能够正常工作,并且能够正确应对各种错误参数! 三、小结 今天我为redis添加了一条简单的命令,并从中了解到了redis的内部抽象及处理命令的流程。 不得不说redis代码的易读性及可扩展性比mssql做得非常好,我只需了解几个文件就能够轻松添加一条命令。 另外这种 learn by hacking的方式的确要比漫无目的的通读代码来得高效,值得继续。 上一条: redis在运维数据分析中的去重统计方式
|