IT技术博客大学习 共学习 共进步

Redis源代码分析

IM鑫爷 2011-12-18 22:05:51 浏览 4,341 次

    一直有打算写篇关于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、hash

     rdb.c //redis数据文件处理

     redis.c //程序主要文件

     replication.c //数据同步master-slave

     sds.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中,在理想的情况下只需一次就能定位命令的处理函数。

     void populateCommandTable(void) {

     int j;

     int numcommands = sizeof(readonlyCommandTable)/sizeof(struct redisCommand);

     for (j = 0; j < numcommands; j++) {

     struct redisCommand *c = readonlyCommandTable+j;

     int retval;

     retval = dictAdd(server.commands, sdsnew(c->name), c);

     assert(retval == DICT_OK);

     }

     }

    3、对参数的解析,redis-server有一个参数(可以不需要),这个参数是指定配置文件路径,然后由函数loadServerConfig(config.c:28)加载所有配置

     if (argc == 2) {

     if (strcmp(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、客户端链表、公共字符串等。

     void initServer() {

     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、程序正常退出。因此redis将数据写到文件缓冲区之后会判断是否需要刷到硬盘,server.appendfsync有两种方式,第一种(APPENDFSYNC_ALWAYS):无条件刷新,即每次写文件都会保存到硬盘,第二种(APPENDFSYNC_EVERYSEC):每隔一秒保存到硬盘。

    10、接下来我们开始讲解aeProcessEvents(ae.c:275)的处理流程,首先我们来回顾一下第5节设置的定时器和监听socket事件处理,其中socket事件处理会回调acceptTcpHandler(networking.c:410)和定时器回调函数serverCron(redis.c:519),在aeProcessEvents的内部有两部分需要处理,第一部分是调用aeApiPoll判断socket是否有数据可读,整个服务端的socket里面要分监听socket和客户端socket,当有客户端链接服务器时,会触发监听socket的事件处理函数,也就是acceptTcpHandler,而acceptTcpHandler会去调用createClient(networking.c:13)创建客户端对像,然后为这个客户端设置事件处理函数readQueryFromClient(networking.c:827),所以当客户端有消息时就会触发客户端socket 事件处理函数,处理数据部分讲在后面详细讲解,接下来的第二部分就是定时器,每次在socket部分处理完后就用调用processTimeEvents(ae.c:212)来处理定时器,那么内部实现也很简单,当设置定时器的时候就会计算好应该触发的时间,所以这里就只需要判断当前时间是否大于或者等于应该触发的时间即可。那么这个定时器到底做了什么呢?请继续第11节。

    11、我们继续跟踪源代码serverCron(redis.c:519),整个函数分为七个部分,第一部分:在服务端打印一些关于DB的信息(包括key数量,内存使用量等);第二部分:判断DB的hash table是否需要扩展大小tryResizeHashTables(redis.c:432);第三部分:关闭太长时间没有通信的链接closeTimedoutClients(networking.c:629);第四部分:保存rdb文件rdbSaveBackground(rdb.c:507),当然也是在需要保存的情况才会保存,即设置save参数;第五部分:清除过期的key,当然这里不是清除全部,他只是随机取出一些activeExpireCycle(redic.c:477);第六部分:虚拟内存交换部分,将一部分key转到虚拟内存中,这里的key也是随机抽取的, vmSwapOneObjectBlocking(vm.c:521);第七部分:主从同步,replicationCron(replication.c:500)。

    12、在第10节中我们讲到客户端socket处理函数readQueryFromClient,这里我们一层层分析,首先是从客户端读取数据,然后调用processInputBuffer,在内部先是判断类型,然后调用processInlineBuffer或者processMultibulkBuffer解析参数,解析后的参数由argv存储参数,其类型是一个指向指针的指针,其中argv[0]是命令名称,后面就是命令参数,argc存储参数数量;然后调用processCommand(redis.c:979)处理命令,在内部调用lookupCommand(redis.c:940)获取命令对应的函数,然后调用freeMemoryIfNeeded(redis.c:1385)判断是否需要释放一些内存,接下来就是调用call(redis.c:954)去执行命令,执行命令后会调用feedAppendOnlyFile(aof.c:137)把命令行保存到aofbuf中,然后判断是否需要同步数据到slave,如果需要则调用replicationFeedSlaves(replication.c:10),接下来就是判断是否需要将数据发送到监控端,如果需要则调用replicationFeedMonitors(replication.c:82),到这里整个服务流程就结束了。至于每条命令如何执行,大家可以去查看以t_开头的几个文件。下面是一张整个服务的流程图。

    \"\"

建议继续学习

  1. redis源代码分析 - persistence (阅读 32,104)
  2. Redis消息队列的若干实现方式 (阅读 11,927)
  3. 基于Redis构建系统的经验和教训 (阅读 10,383)
  4. 浅谈redis数据库的键值设计 (阅读 9,222)
  5. redis运维的一些知识点 (阅读 8,522)
  6. redis在大数据量下的压测表现 (阅读 8,203)
  7. Redis和Memcached的区别 (阅读 7,944)
  8. redis 运维实际经验纪录之一 (阅读 7,583)
  9. Redis作者谈Redis应用场景 (阅读 7,544)
  10. 记Redis那坑人的HGETALL (阅读 7,323)