Redis 是典型的 单 Reactor 单线程 的事件驱动模型,其服务端的启动也就是事件循环的建立过程,服务端启动的入口是 server.c文件内的主函数。
关于事件驱动的部分内容比较多,后面我会单独分出一到两篇文章介绍,本文主要关注Redis-server的启动流程即可。
当我们在命令行输入了redis-server命令后,Redis的服务端就会启动起来并对外提供服务了,这个时候我们就可以通过客户端连接Redis的服务端进行操作了。接下来我们通过源码来看一下,这个过程中都发生了什么事,才能让高并发必备的Redis启动起来。Redis启动流程涉及到的函数都比较长,我会截取一些关键部分进行分析,然后最后在做一个统一的梳理。
1.main函数的流程
首先我们先来到server.c文件,该文件的main函数就是Redis启动的入口,这个函数的内容很多,看起来有一些没有头绪,这个时候我们就说一下看源码的方法,首先源码的方法上一般会有一些注释,来对这个方法的功能出入参进行简单的介绍,这个其实对于我们刚开始看一个不熟悉的源码是很重要的,如果对一个方法的内容不了解的情况下,一定要看一下作者的注释,再通过注释了解了这个方法的大概功能以后,我们就可以顺着方法的功能来对方法内的代码进行分析了。在这个过程中可能有一些代码的逻辑我们一下并不能看懂,这个时候不要过于纠结,可以先打一个标记后暂时跳过,很多代码的前后关联性很强,可能在你看完了后面的代码后,前面看不懂的部分水到渠成的就明白是什么意思了。源码切忌按部就班,逐行看懂的钻牛角尖,这样很容易事倍功半,还会打击看源码的信心。
言归正传,我们来看一下这个方法主要都做了些什么?首先源码里面的很多函数都是见名知意的,比如这个initServerConfig函数,一眼看去就晓得是初始化服务端配置,但是具体初始化什么配置我们并不知道,这个时候我们可以在这行代码上加一行注释,等看完当前方法的主要流程后,在针对里面的重要方法和步骤进行分析。接下来我们我们按照这种方法标记出加载服务端配置的loadServerConfig方法,初始化服务端的initServer方法和事件模型的主函数aeMain方法。在我们对这些方法进行分析之前,我们先来梳理下Redis入口函数的逻辑,加载配置,根据配置进行一些必要的初始化工作,调用事件模型的入口函数。
int main(int argc, char **argv) {
...
//初始化 server 端的各项配置
initServerConfig();
.....
//加载配置文件redis.conf 初始化服务端的配置 ,eg:设置redis的监听端口 6379
loadServerConfig(server.configfile, config_from_stdin, options);
......
//创建事件循环对象,配置server用来处理socket event 的函数 & 处理 定时 event 的函数
initServer();
......
//启动事件循环的处理线程,到了这里,redis就可以接收客户端的请求命令了
aeMain(server.el);
2.初始化配置
首先我们来看一下initServerConfig
函数,这里面比较重要的一行代码就是调用了populateCommandTable函数,我们来看一下这个函数做了些什么?
void initServerConfig(void) {
...
populateCommandTable();
....
}
其实这里的逻辑很简单,主要是把server.c文件顶部硬编码的redisCommand
类型的列表填充为redis的命令。
for (j = 0; j < numcommands; j++) {
if (populateCommandTableParseFlags(c, c->sflags) == C_ERR)
serverPanic("Unsupported command flag");
retval1 = dictAdd(server.commands, sdsnew(c->name), c);
}
那么接下来我们就可以来看一下redisCommand
长什么样子。redisCommand 是一条命令的存储结构,所谓的一条命令就是我们可以在Redis客户端输入然后发送给Redis服务端执行的指令。其中 name 属性为命令名称,proc 为函数的指针,就是通过该指针建立了命令名称与处理函数之间的映射关系。
struct redisCommand {
char *name;
redisCommandProc *proc;
....
};
redisCommandTable
是一个硬编码的数组,位于server.c文件中,里面定义了redis服务端可以接收的所有命令和对应的处理函数,通过这个数组我们可以很快定位到某个命令的入口函数。这对于我们看源码来说很关键。
3.加载服务端配置文件
接下来的loadServerConfig
函数从指定的文件名加载服务器配置。在加载完文件以后,还会继续加载stdin输入的内容和执行redis启动命令的时候输入的参数,加载完成后,将他们追加到内存中的redis.conf配置文件。这也就解释了为什么我们在redis-server指令后面追加的命令参数会覆盖配置文件的配置。因为先加载的配置文件的配置后加载的命令行的配置,这样当加载命令行的配置时,就会对内存中已经有的配置进行覆盖操作。
....
/* 加载配置文件 */
if (filename) {
while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
config = sdscat(config,buf);
fclose(fp);
}
/* 将stdin的输入内容追加到加载的配置文件 */
if (config_from_stdin) {
fp = stdin;
while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
config = sdscat(config,buf);
}
/* 将命令行参数追加到加载的配置文件 */
if (options) {
config = sdscat(config,"\n");
config = sdscat(config,options);
}
...
4.初始化server
这个函数有点长,不过没关系,我们可以配合方法头和方法内的注释来阅读代码进行分析。
- 首先根据上一个方法从配置文件,命令行加载的配置(包括一些:定时任务执行频率,客户端链表结构,从机链表结构等等)对服务端进行初始化。
- 接下来调用aeCreateEventLoop函数创建事件循环实例,不了解事件循环实例不要紧,我们先继续往下看。
- zmalloc函数式Redis里面很重要的一个函数,就是分配内存,所以很明显这里的操作就是为数据库分配内存。
- 绑定服务端socket监听端口,通过unix套接字实现监听
- 初始化redis的数据库结构【redisDB】,默认一共有16个。
- 指定一下定时事件的处理函数为【serverCron】,包括:主从节点&集群模式 各个节点的定时通信。
- 创建一个事件处理程序【acceptTcpHandler】,用于接受TCP和Unix域套接字中的新连接。
- 判断当因为客户端命令发生阻塞的时候,创建一个可读事件。
- 设置每次事件处理之前需要进行的操作为【beforeSleep】,他会对一些数据进行淘汰,还会设置事件处理之后的函数【afterSleep】。
void initServer(void) {
/* 从配置系统设置默认值后的初始化。
* 包括一些:定时任务执行频率,客户端链表结构,从机链表结构等等。*/
server.aof_state = server.aof_enabled ? AOF_ON : AOF_OFF;
....
//此处省略一些参数的初始化
...
//创建事件循环实例
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
if (server.el == NULL) {
exit(1);
}
//为数据库分配内存
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
/* 绑定服务端socket监听端口 */
if (server.port != 0 &&
listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
exit(1);
....
/* 此处省略了通过unix套接字实现监听 */
/* 初始化redis的数据库结构【redisDB】,默认一共有16个。 */
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType,NULL);
//此处省略了部分数据库参数的初始化
listSetFreeMethod(server.db[j].defrag_later,(void (*)(void*))sdsfree);
}
.....
//指定一下定时事件的处理函数为【serverCron】,包括:主从节点&集群模式 各个节点的定时通信。
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
for (j = 0; j < server.ipfd_count; j++) {
/* 创建一个事件处理程序【acceptTcpHandler】,用于接受TCP和Unix域套接字中的新连接。
* 【redis的单线程就是在这里体现的,就创建了这一个线程处理请求】*/
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR){
serverPanic("Unrecoverable error creating server.ipfd file event.");
}
}
...
//当因为客户端命令发生阻塞的时候,创建一个可读事件。
if (aeCreateFileEvent(server.el, server.module_blocked_pipe[0], AE_READABLE,
moduleBlockedClientPipeReadable,NULL) == AE_ERR) {
...
}
//设置每次事件处理之前需要进行的操作为【beforeSleep】,他会对一些数据进行淘汰
aeSetBeforeSleepProc(server.el,beforeSleep);
//设置每次事件处理之后需要进行的操作
aeSetAfterSleepProc(server.el,afterSleep);
....
}
5.创建事件循环实例
在上一步初始化服务端的代码分析过程中,我们看到了一个函数aeCreateEventLoop,这个函数其实是创建事件循环对象,接下来我们来看一下aeCreateEventLoop函数的流程,其实如果你以前有过看源码的经验,一眼就能看出整个方法就是根据传入的 size创建两个数组加一些其他的参数给事件循环eventLoop对象赋能,至于这两个数组是做什么的,我们暂时不用关心,后面到了具体细节分析的时候我们会详细说明。
在Redis源代码中对于对于setsize的定义:setsize:server.maxclients+CONFIG_FDSET_INCR
。其中:maxclients
代表用户配置的最大连接数,可以在启动的时候通过--maxclients
指定,默认为10000;CONFIG_FDSET_INCR
参数的大小为128,给Redis预留一些安全的空间。
...
eventLoop->events = zmalloc(sizeof(aeFileEvent) * setsize);
eventLoop->fired = zmalloc(sizeof(aeFiredEvent) * setsize);
//省略一些事件循环参数的初始化
return eventLoop;
//省略错误处理的逻辑
6.启动事件循环的处理线程
终于到了入口函数里面的最后一个方法aeMain,可以看到这个函数的核心逻辑就是不停的循环调用aeProcessEvents
方法,直到事件循环对象的stop属性不等于0。aeProcessEvents
方法比较长,我就不再上代码了,直接做一个梳理:
aeProcessEvents()
函数会处理两种事件,分别是定时触发的事件和Socket I/O触发的读写事件。因为 redis 是单线程的,这两种事件不可以同时处理,所以该函数中有一种时间分片的策略,简单来说就是首先计算当前时间距离最近的定时事件触发时的时间差 T,然后调用 aeApiPoll()
设置轮询 Socket 的超时时间为 T,在超时时间内只处理 Socket 读写事件,超时时间到了后再调用 processTimeEvents()
函数处理定时事件。这个函数最终会返回处理的事件个数。这个函数后面我会在Redis的多路复用模型里面展开分析源码。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
//当事件组没有停止的时候
while (!eventLoop->stop)
//就会执行这个函数,所以看这个函数的逻辑
aeProcessEvents(eventLoop, AE_ALL_EVENTS | AE_CALL_BEFORE_SLEEP | AE_CALL_AFTER_SLEEP);
}
启动了事件处理线程以后,redis就可以接收客户端的请求和命令了。
7.总结
至此,redis-server就启动起来了,接下来就是处理客户端请求的流程,我将会在下一篇文章分析。最后我们再来回顾下server的启动流程:
- 初始化配置
- 加载server.c硬编码的命令列表
- 处理启动的时候通过命令行带过来的配置
- 加载服务端配置文件
- 初始化server
- 从配置系统设置默认值后的初始化。包括一些:定时任务执行频率,客户端链表结构,从机链表结构等等。
- tls 相关的配置
- 创建事件循环实例
- 绑定服务端socket监听端口,通过unix套接字实现监听
- 初始化redis的数据库结构【redisDB】,默认一共有16个。
- 指定一下定时事件的处理函数为【serverCron】,包括:主从节点&集群模式 各个节点的定时通信。
- 创建一个事件处理程序【acceptTcpHandler】,用于接受TCP和Unix域套接字中的新连接。
- 当因为客户端命令发生阻塞的时候,创建一个可读事件。
- 设置每次事件处理之前需要进行的操作为【beforeSleep】,他会对一些数据进行淘汰,【一些内存淘汰策略相关的内容】
- 创建事件处理器,将
acceptTcpHandler
赋值给文件事件处理的读指针 - 启动事件循环的处理线程
- 处理定时触发的事件
- 处理socket io触发的事件