Redis源代码分析一直有打算写篇关于redis源代码分析的文章,一直很忙,还好最近公司终于闲了一点,总算有点时间学习了,于是终于可以兑现承诺了,废话就到此吧,开始我们的源代码分析,在文章的开头我们把所有服务端文件列出来,并且标示出其作用:adlist.c //双向链表ae.c //事件驱动ae_epoll.c //epoll接口, linux用ae_kqueue.c //kqueue接口, freebsd用ae_select.c //select接口, windows用anet.c //网络处理aof.c //处理AOF文件config.c //配置文件解析db.c //DB处理dict.c //hash表intset.c //转换为数字类型数据multi.c //事务,多条命令一起打包处理networking.c //读取、解析和处理客户端命令object.c //各种对像的创建与销毁,string、list、set、zset、hashrdb.c //redis数据文件处理redis.c //程序主要文件replication.c //数据同步master-slavesds.c //字符串处理sort.c //用于list、set、zset排序t_hash.c //hash类型处理t_list.c //list类型处理t_set.c //set类型处理t_string.c //string类型处理t_zset.c //zset类型处理ziplist.c //节省内存方式的list处理zipmap.c //节省内存方式的hash处理zmalloc.c //内存管理上面基本是redis最主要的处理文件,部分没有列出来,如VM之类的,就不在这里讲了。
首先我们来回顾一下redis的一些基本知识:1、redis有N个DB(默认为16个DB),并且每个db有一个hash表负责存放key,同一个DB不能有相同的KEY,但是不同的DB可以相同的KEY;2、支持的几种数据类型:string、hash、list、set、zset;3、redis可以使用aof来保存写操作日志(也可以使用快照方式保存数据文件)对于数据类型在这里简单的介绍一下(网上有图,下面我贴上图片可能更容易理解)1、对于一个string对像,直接存储内容;2、对于一个hash对像,当成员数量少于512的时候使用zipmap(一种很省内存的方式实现hash table),反之使用hash表(key存储成员名,value存储成员数据);3、对于一个list对像,当成员数量少于512的时候使用ziplist(一种很省内存的方式实现list),反之使用双向链表(list);4、对于一个set对像,使用hash表(key存储数据,内容为空)5、对于一个zset对像,使用跳表(skip list),关于跳表的相关内容可以查看本blog的跳表学习笔记;下面正式进入源代码的分析1、首先是初始化配置,initServerConfig(redis.c:759)void initServerConfig() {server.port = REDIS_SERVERPORT;server.bindaddr = NULL;server.unixsocket = NULL;server.ipfd = -1;server.sofd = -1;2.在初始化配置中调用了populateCommandTable(redis.c:925)函数,该函数的目地是将命令集分布到一个hash table中,大家可以看到每一个命令都对应一个处理函数,因为redis支持的命令集还是蛮多,所以如果要靠if分支来做命令处理的话即繁琐效率还底,因此放到hash table中,在理想的情况下只需一次就能定位命令的处理函数。
voidpopulateCommandTable(void) {int j;intnumcommands =sizeof(readonlyCommandTable)/sizeof(structredisCommand);for (j = 0; j <numcommands; j++) {structredisCommand *c = readonlyCommandTable+j;intretval;retval = dictAdd(mands, sdsnew(c->name), c);assert(retval == DICT_OK);}}3、对参数的解析,redis-server有一个参数(可以不需要),这个参数是指定配置文件路径,然后由函数loadServerConfig(config.c:28)加载所有配置if (argc == 2) {if (s trcmp(argv[1], “-v”) == 0 ||strcmp(argv[1], “–version”) == 0) version();if (strcmp(argv[1], “–help”) == 0) usage(); resetServerSaveParams();loadServerConfig(argv[1]);4、初始化服务器initServer(redis.c:836), 该函数初始化一些服务器信息,包括创建事件处理对像、db、socket、客户端链表、公共字符串等。
voidinitServer() {int j;signal(SIGHUP, SIG_IGN);signal(SIGPIPE, SIG_IGN);setupSignalHandlers();//设置信号处理if (server.syslog_enabled) {openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT, server.syslog_facility);}5、在上面初始化服务器中有一段代码是创建事件驱动,aeCreateTimeEvent是创建一个定时器,下面创建的定时器将会每毫秒调用 serverCron函数,而aeCreateFileEvent是创建网络事件驱动,当server.ipfd和server.sofd有数据可读的情况将会分别调用函数acceptTcpHandler和acceptUnixHandler。
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);if (server.ipfd> 0&&aeCreateFileEvent(server.el,server.ipfd,AE_READABLE, acceptTcpHandler,NULL) == AE_ERR) oom(“creating file event”);if (server.sofd> 0&&aeCreateFileEvent(server.el,server.sofd,AE_READABLE, acceptUnixHandler,NULL) == AE_ERR) oom(“creating file event”);6、接下来就是初始化数据,如果开启了AOF,那么会调用loadAppendOnlyFile(aof.c:216)去加载AOF文件,在AOF 文件中存放了客户端的命令,函数将数据读取出来然后依次去调用命令集去处理,当AOF文件很大的时候势必为影响客户端的请求,所以每处理1000条命令就会去尝试接受和处理客户端的请求,其代码在aof.c第250行; 但是如果没有开启AOF并且有rdb 的情况,会调用rdbLoad(redis.c:873)尝试去加载rdb文件,理所当然的在加载rdb文件的内部也会考虑文件太大而影响客户端请求,所以跟AOF一样,每处理1000条也会尝试去接受和处理客户端请求。
7、当所有初始化工作做完之后,服务端就开始正式工作了aeSetBeforeSleepProc(server.el,beforeSleep);aeMain(server.el);8、大家都知道redis是单线程模式,所有的请求、处理都是在同一个线程里面进行,也就是一个无限循环,在这个无限循环的内部有两件事要做,第一件就是调用通过aeSetBeforeSleepProc函数设置的回调函数,第二件就是开始接受客户端的请求和处理,所以我们可以在第7节看到设置了回调函数为beforeSleep,但是这个beforeSleep到底有什么作用呢?我们在第9节再详细讲述。
对于aeMain(ae.c:375)就是整个程序的主要循环。
void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;while (!eventLoop->stop) {if (eventLoop->beforesleep != NULL)eventLoop->beforesleep(eventLoop);aeProcessEvents(eventLoop, AE_ALL_EVENTS);}}9、在beforeSleep内部一共有三部分,第一部分对vm进行处理(即第一个if 块),这里我们略过;第二部分是释放客户端的阻塞操作,在 redis里有两个命令BLPOP和BRPOP需要使用这些操作(弹出列表头或者尾,实现方式见t_list.c:862行的 blockingPopGenericCommand函数),当指定的key不存在或者列表为空的情况下,那么客户端会一直阻塞,直到列表有数据时,服务端就会去执行lpop或者rpop并返回给客户端,那么什么时候需要用到BLPOP和BRPOP呢?大家平时肯定用redis做过队列,最常见的处理方式就是使用llen 去判断队列有没有数据,如果有数据就去取N条,然后处理,如果没有就sleep(3),然后继续循环,其实这里就可以使用BLPOP或者 BRPOP来轻松实现,而且可以减少请求,具体怎么实现留给大家思考;第三部分就是flushAppendOnlyFile(aof.c:60),这个函数主要目的是将aofbuf的数据写到文件,那aofbuf是什么呢?他是AOF的一个缓冲区,所以客户端的命令都会在处理完后把这些命令追加到这个缓冲区中,然后待一轮数据处理完之后统一写到文件(所以aof也是不能100%保证数据不丢失的,因为如果当redis正在处理这些命令的情况下服务就挂掉,那么这部分的数据是没有保存到硬盘的),大家都知道写数据到文件并不是立即写到硬盘,只是保存到一个文件缓冲区中,什么情况下会把缓冲区的数据转到硬盘呢?只要满足如下三种条件的一种就能将数据真正存到硬盘:1、手动调用刷新缓冲区;2、缓冲区已满;3、程序正常退出。