Redis是用C语言实现的,首先,你当然应该从main函数开始读起。但我们在读的时候应该抓住一条主线,也就是当我们向Redis输入一条命令的时候,代码是如何一步步执行的。这样我们就可以先从外部观察,尝试执行一些命令,在了解了这些命令执行的外部表现之后,再钻进去看对应的源码是如何实现的。要想读懂这些代码,首先我们需要理解Redis的事件机制。而且,一旦理解了Redis的事件循环(Event Loop)的机制,我们还会搞明白一个有趣的问题:为什么Redis是单线程执行却能同时处理多个请求?(当然严格来说Redis运行起来并非只有一个线程,但除了主线程之外,Redis的其它线程只是起辅助作用,它们是一些在后台运行做异步耗时任务的线程)
为了表述清楚,本文按照如下思路进行:
- 先概括地介绍整个代码初始化流程(从main函数开始)和事件循环的结构;
- 再概括地介绍对于Redis命令请求的处理流程;
- 重点介绍事件机制;
- 对于前面介绍的各个代码处理流程,给出详细的代码调用关系,方便随时查阅;
根据这样几部分的划分,如果你只想粗读大致的处理流程,那么只需要阅读前两个部分就可以了。而后两部分则会深入到某些值得关注的细节。
初始化流程和事件循环概述
Redis源码的main函数在源文件server.c中。main函数开始执行后的逻辑可以分为两个阶段:
- 各种初始化(包括事件循环的初始化);
- 执行事件循环。
这两个执行阶段可以用下面的流程图来表达(点击看大图):
首先,我们看一下初始化阶段中的各个步骤:
- 配置加载和初始化。这一步表示Redis服务器基本数据结构和各种参数的初始化。在Redis源码中,Redis服务器是用一个叫做redisServer的struct来表达的,里面定义了Redis服务器赖以运行的各种参数,比如监听的端口号和文件描述符、当前连接的各个client端、Redis命令表(command table)配置、持久化相关的各种参数,等等,以及后面马上会讨论的事件循环结构。Redis服务器在运行时就是由一个
redisServer
类型的全局变量来表示的(变量名就叫server
),这一步的初始化主要就是对于这个全局变量进行初始化。在整个初始化过程中,有一个需要特别关注的函数:populateCommandTable
。它初始化了Redis命令表,通过它可以由任意一个Redis命令的名字查找该命令的配置信息(比如该命令接收的命令参数个数、执行函数入口等)。在本文的第二部分,我们将会一起来看一看如何从接收一个Redis命令的请求开始,一步步执行到来查阅这个命令表,从而找到该命令的执行入口。另外,这一步中还有一个值得一提的地方:在对全局的redisServer
结构进行了初始化之后,还需要从配置文件(redis.conf)中加载配置。这个过程可能覆盖掉之前初始化过的redisServer
结构中的某些参数。换句话说,就是先经过一轮初始化,保证Redis的各个内部数据结构以及参数都有缺省值,然后再从配置文件中加载自定义的配置。 - 创建事件循环。在Redis中,事件循环是用一个叫
aeEventLoop
的struct来表示的。「创建事件循环」这一步主要就是创建一个aeEventLoop
结构,并存储到server
全局变量(即前面提到的redisServer
类型的结构)中。另外,事件循环的执行依赖系统底层的I/O多路复用机制(I/O multiplexing),比如Linux系统上的epoll机制[1]。因此,这一步也包含对于底层I/O多路复用机制的初始化(调用系统API)。 - 开始socket监听。服务器程序需要监听才能收到请求。根据配置,这一步可能会打开两种监听:对于TCP连接的监听和对于Unix domain socket[2]的监听。「Unix domain socket」是一种高效的进程间通信(IPC[3])机制,在POSIX规范[4]中也有明确的定义[5],用于在同一台主机上的两个不同进程之间进行通信,比使用TCP协议性能更高(因为省去了协议栈的开销)。当使用Redis客户端连接同一台机器上的Redis服务器时,可以选择使用「Unix domain