鉴于Redis目前实在很火,而且传说核心代码是4万多行c,让人时不时有些想要一探究竟的小冲动。
找个好天气,行动吧!下载git,留几句阅读源码比较有用的git命令备用。
- git clone https://github.com/antirez/redis.git
- git log > ../redis.log
- git checkout -b <branch-name> <commit-id>
- git branch
- git checkout <branch-name>
第1句话是下载redis源码,第2句是输出日志,第3句新建并切换分支,第4句列出全部分支,第5句切换分支。
有了这5句话,git部分基本就没啥问题了。
个人阅读源码的习惯是不管三七二十一先切换到第一个commit。刚才已经说了切换分支的命令了,git checkout -b first-commit ed9b544e10b84cd43348ddfab7068b610a5df1f7。<commit-id>的来源,打开redis.log,最抢眼的就是它了!然后,我还会做的一件事是统计一下源码有几行:)
cat *.c *.h | wc -l,一看这第一个commit的redis一共有6232行源码,看来代码不长,顿觉信心百倍。
从哪开看呢?我想README会是个好地方,结果它推荐查看doc/README.html,那就看吧。整理一下我看到的知识点:
- In Redis keys are just strings too, but the associated values can be Strings, Lists and Sets。这里需要看一下value是String, List, Set在redis里面是如何实现的,虽然估计是直接用指针实现。
- Redis writes a dump of the dataset on disk asynchronously. 异步回写磁盘也需要多加关注。
- the idea is to provide atomic primitives in order to make the programmer able to use redis with locking free algorithms. OK,原子操作的实现也是Redis的杀手锏。
- Another synchronization primitive is the support for multiple DBs.多DB的知识,也作为是Redis的一个特色,不过设计这个东西究竟是何意图,对于初学者来说还是值得推敲。
- 数据类型的支持,目前第一版本是Striing, List, Set。
下一个是谁呢?Makefile!来看看redis会编译出一些什么东西。看到all的依赖后面跟的是redis-server redis-benchmark redis-cli,就知道最后会编译出来的是这三个东西。然后看redis-server的依赖,稍微动一下小脑袋就会发现会有adlist.c ae.c anet.c dict.c redis.c sds.c zmalloc.c这几个源文件会成为待会儿的下酒菜。
在看redis.c这个98KB的大家伙之前,先在源码中四周游荡一下,看看client-libraries,里面藏了很多其他语言编写的代码,我就来看看python的吧,里面只有一个redis.py,打开文件它的描述是A client for the Redis daemon。代码结构比较简单,在几个异常类的定义之后,就只有一个Redis类,里面就藏了很多的redis命令了,每个都有使用案例的,还是非常容易看的。再瞄一眼redis-cli.c,有command定义,以及main函数,一扫而过,终归就是建立与服务器的通信,然后发命令,接受服务器的回复。晚些再来细读。
饶了好大一个弯子,终于到了redis.c,3037行代码,直接开看就是了。边看边摘抄:
- /* A redis object, that is a type able to hold a string / list / set */
- typedef struct redisObject {
- int type;
- void *ptr;
- int refcount;
- } robj;
后续在1303行的地方还定义了一堆函数对这个object进行管理。
- typedef struct _redisSortObject {
- robj *obj;
- union {
- double score;
- robj *cmpobj;
- } u;
- } redisSortObject;
这一段代码有一点继承的味道了。
- li = listGetIterator(server.clients,AL_START_HEAD);
- if (!li) return;
- while ((ln = listNextElement(li)) != NULL) {
- c = listNodeValue(ln);
- if (!(c->flags & REDIS_SLAVE) && /* no timeout for slaves */
- (now - c->lastinteraction > server.maxidletime)) {
- redisLog(REDIS_DEBUG,"Closing idle client");
- freeClient(c);
- }
- }
- listReleaseIterator(li);
- static void createSharedObjects(void) {
- shared.crlf = createObject(REDIS_STRING,sdsnew("\r\n"));
- shared.ok = createObject(REDIS_STRING,sdsnew("+OK\r\n"));
- ...
- shared.select9 = createStringObject("select 9\r\n",10);
- }
似乎与磁盘回写相关的代码在serverCron这个函数里面,为何做如此判断呢?印象中cron是一个linux计划安排的程序。
Redis号称的多server,貌似是有主从之分,看到replicationFeedSlaves,我已经基本把slave这个词映入脑海了。
最后的一个大问题就是,Redis如何保证原子操作的,我专门看了一下incrCommand但是没有看出来,具体是如果做到原子操作的。
看redis.c之后,那么下一个目标就是sds.c了,因为redis里面的string总号称自己是sds string,至于什么是sds string,我们就通过sds.c这个源码来一探究竟吧。看来antirez这哥们着实是重写了一个string操作的库函数,代码不长329行。或许除了提供特定的字符串操作函数之外,最大的特色就是使用zmalloc.c里面内存操作的包装函数来做了内存管理。给我的感觉是,redis为所有的对象管理建立了一套特有的管理方案,string作为一个对象也绝不例外。
到了zmalloc.c东西就比较简单了,82行代码,封装了初始的c语言内存分配的函数malloc,realloc,free,然后维护一个内存使用状况的变量used_memory。
最后配合上封装socket的anet.c,消息循环的ae.c;adlist.c实现链表操作,dict.c实现哈希表操作来为set提供基础之后,redis大体的内容就结束了。
现在想来redis之所以支持原子操作,难道是因为,一次一个服务器只和一个客户端进行连接,而且服务器是运行在单线程模式下才得以保证的?
后续切回到Redis的最新代码上去,git checkout unstable,发现核心源码已经放入到src中,虽然多了bio, aof, cluster等诸多内容,但是总的来说,框架性的东西大体变化都不大。
总结:总的来说,本次源码阅读的宗旨是想看一下redis究竟长啥样,看完之后,积累的知识主要包括的C语言的面向对象设计,以及基本数据结构和算法的实际实现。
前文并没有讲解算法与数据结构,其实redis里面还是有些这玩意儿的,比如:双向链表(adlist.c,重敲一遍花了2小时),哈希表(dict.c,对其他文件的关联度相对较大,没能重敲完),快速排序(pqsort.c,高度优化,真心有engineering的味道)。