一、写在开始
Redis是一个事件驱动的数据库服务器,服务启动后通过不断的处理时间事件和文件事件来提供数据存储和查询服务。
时间事件:Redis在指定的时间执行的一些逻辑,如:数据备份、主从同步等等。
文件事件:Redis服务器通过套接字与客户端(或其他Redis服务器)进行连接,文件事件就是服务器对套接字操作的抽象。Redis与客户端(或其他Redis服务器)的通信会产生相应的文件事件,服务器通过监听和处理这些事件来完成一系列网络操作,保证服务的运转。
当在网络上搜索Redis文件事件模型的时候,诸如“单线程模型”、“IO多路复用”、“命令请求处理器/连接应答处理器/命令回复处理器”等这样的词汇时常会出现在搜索结果中,下图也常常是用来描述Redis事件模型的逻辑图。
但Redis真正在代码层面对文件事件的执行是如何实现的,对我个人而言这块一直是个黑盒。因此最近通过阅读了一下Redis源代码对这块的实现逻辑进行了一下了解。本文主要是将我读代码的过程和收获进行一下梳理和总结。
我个人对C语言的了解很一般,Redis中很多复杂的逻辑,其实在读代码的过程中也没有完全看懂,但是大概逻辑和脉络还是能了解一些,对于了解Redis的文件事件模型的事件,这差不多也足够了😂😂😂
本文主要阐述文件事件相关的逻辑,文中涉及到的截图和代码来自于黄健宏老师的Redis设计与实现(第二版)及其他本人进行注释的redis3.0的代码。
二、看代码
Redis代码中详细的各种数据结构、方法逻辑本文就不进行阐述了(主要是我也看不懂😂),本文主要讲述的只是Redis文件事件的整体脉络和执行逻辑。
1. Redis启动
Redis的执行入口是src/redis.c文件中的main方法,如下所示。
int main(int argc, char **argv) {
struct timeval tv;
/* We need to initialize our libraries, and the server configuration. */
// 初始化库
//这里省略很多。。。。
// 初始化服务器配置
initServerConfig();
//这里省略很多。。。。
// 创建并初始化服务器数据结构
initServer();
// 如果服务器是守护进程,那么创建 PID 文件
if (server.daemonize) createPidFile();
//这里省略很多。。。。
// 运行事件处理器,一直到服务器关闭为止
aeMain(server.el);
// 服务器关闭,停止事件循环
aeDeleteEventLoop(server.el);
return 0;
}
这个main方法的主体逻辑很长,其内部主要进行一些数据的初始化、配置文件的载入、磁盘数据回复、端口启动等操作。如上所示代码将很多详细逻辑进行了省略,只保留了与本文相关的一些逻辑。
上面代码中的server是redisServer这个结构体的声明变量,该结构体也是整个Redis的核心,redis所存储的数据及各种核心配置也全部都在这个结构体的实例当中。关于redisServer数据结构的详细介绍见开篇提到的书籍。
代码中的initServerConfig()和initServer()方法的逻辑主要是对redisServer这个结构体的内部成员的初始化,asMain方法就是整个服务Redis服务运行的开始,这个方法的执行意味着Redis服务真正的开始对外提供服务。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件处理前执行的函数,那么运行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 开始处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
如上展示的是asMain方法的逻辑(未删减)。其逻辑不复杂,由一个通过stop变量控制的无限循环构成,循环内每次执行的逻辑是调用aeProcessEvents()方法。
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* Nothing to do? return ASAP */
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
//文件事件执行开始
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
//这里省略了tvp获取的逻辑。。。。。
// 处理文件事件,阻塞时间由 tvp 决定
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
// 从已就绪数组中获取事件
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;
// 读事件
if (fe->mask & mask & AE_READABLE) {
// rfired 确保读/写事件只能执行其中一个
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
// 写事件
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
processed++;
}
}
//文件事件执行结束
// 执行时间事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
/* return the number of processed file/time events */
return processed;
}
如上显示的是aeProcessEvents()方法的执行逻辑(省略了部分逻辑),从大体脉络上可以看出,方法中依次执行文件事件、时间事件。
执行文件事件的逻辑是:
- 首先通过调用aeApiPoll方法来获取已经就绪的事件
- 通过遍历numevents来获取对应的待执行的文件事件
- 遍历逻辑中,根据事件的类型(读事件 OR 写事件)分别调用rfileProc和wfileProc方法来完成对应事件的执行。
通过阅读上述的代码,我们对Redis从启动到文件事件执行的整个流程进行了一下概览,整个流程如图所示:
搞清楚整体的脉络以后,回顾上述的流程,其中不免有很多细节和疑问,如:
- 为啥通过aeApiPoll方法就能获取就绪的事件,这些事件是什么时候被监听的?
- 为啥通过遍历numevents就能获取到事件,这玩意不就是个整数数组吗?
- 读事件、写事件是啥?
- rfileProc和wfileProc对应的执行逻辑是啥?
- 代码里aeFileEvent是啥?
- eventLoop又是啥?
- eventLoop->events里面放的是啥?
- ...(好多问题)
Chill man, 咱们慢慢来,带着这些疑问继续往下看。
2. 读事件、写事件
Redis中的文件事件分为两类,分别是读事件和写事件。回顾前面文件事件的定义:
文件事件:Redis服务器通过套接字与客户端(或其他Redis服务器)进行连接,文件事件就是服务器对套接字操作的抽象。Redis与客户端(或其他Redis服务器)的通信会产生相应的文件事件,服务器通过监听和处理这些事件来完成一系列网络操作,保证服务的运转。
用大白话来描述这段话就是,Redis服务器通过socket(套接字)方式与客户端进行通信(我们把其他Redis服务器也归为客户端),既然是通信那么就需要读和写,因此,从客户端socket进行读客户端输入的内容就是读事件;通过socket向客户端输出(写)一些内容就是写事件,如下图所示。
所以不难理解,所谓的读事件和写事件主要是由于通信本质就是读和写这两个动作,所以在Redis中它会抽象这两个事件概念。
3. aeFileEvent
aeFileEvent是Redis代码中关于文件事件定义的一个结构体,如下所示:
typedef struct aeFileEvent {
// 监听事件类型掩码,
// 值可以是 AE_READABLE 或 AE_WRITABLE ,
// 或者 AE_READABLE | AE_WRITABLE
int mask; /* one of AE_(READABLE|WRITABLE) */
// 读事件处理器
aeFileProc *rfileProc;
// 写事件处理器
aeFileProc *wfileProc;
// 多路复用库的私有数据
void *clientData;
} aeFileEvent;
在这个结构体中封装了读事件处理器、写事件处理器、一个mask的变量,以及多路复用需要用到的数据。其中mask变量是用来标识当前的文件事件是读事件还是写事件;两个处理器是具体的读写处理逻辑,可以看到它是一个指针类型,在系统运行时会指向具体的读写处理函数。
4. aeEventLoop
aeEventLoop同样是Redis代码中定义的一个结构体,本身它是RedisServer这个结构体中的一个成员变量。
typedef struct aeEventLoop {
// 目前已注册的最大描述符
int maxfd; /* highest file descriptor currently registered */
// 目前已追踪的最大描述符
int setsize; /* max number of file descriptors tracked */
// 用于生成时间事件 id
long long timeEventNextId;
// 最后一次执行时间事件的时间
time_t lastTime; /* Used to detect system clock skew */
// 已注册的文件事件
aeFileEvent *events; /* Registered events */
//已就绪的文件事件
aeFiredEvent *fired; /* Fired events */
// 时间事件
aeTimeEvent *timeEventHead;
// 事件处理器的开关
int stop;
// 多路复用库的私有数据
void *apidata; /* This is used for polling API specific data */
// 在处理事件前要执行的函数
aeBeforeSleepProc *beforesleep;
} aeEventLoop;
如上所示,是aeEventLoop的结构体定义,其中我们比较关注的是events和fired,它们都是数组指针,且具体指向的元素是aeFileEvent类型,其中events中存储的是已注册的文件事件,fired中存储的是已就绪的文件事件。
5. aeApiPoll是啥?
通过前面的代码注释我们了解到,通过aeApiPoll函数可以获取到已经就绪的事件,那么具体是怎么获取的呢?它是什么时候开始监听的?
在解决这些疑问之前,我们先简单介绍一下IO多路复用这个概念。
关于IO多路复用的准确定义,大家网上一搜会看到各种文章讲解(这篇个人觉得写的不错)。这里不展开讲,只是用大白话的方式进行一下叙述。
IO通信一步步“进化”的过程是:同步阻塞-->同步非阻塞-->IO多路复用-->异步非阻塞,其通信的性能是依次提升的。
以Redis本身为例,Redis的文件事件处理逻辑是单线程模式运行,当Redis服务器在处理多个socket client端的读或写请求时,由于socket client之间是串行的进行访问和接收请求的,因此Redis服务端会有一个队列来存储各个socket client连接以及他们各自所关联的事件类型(读或写),如下所示。
当以同步阻塞的方式进行处理的时候:
- redis服务端会依次遍历socket client队列;
- 取出每个client,对于读事件,redis通过系统内核获取外接client输入的数据;对于写事件,redis通过系统内核获取外接client是否准备好接收写数据的状态。
- 对于读事件,当redis获取到数据数据后,redis服务端执行后续的处理逻辑;对于写事件,当redis获取到外接client可以接收写数据的时候,对其进行数据输出。
上图所示,是redis在依次遍历client队列时的执行示意图。因为是单线程的方式进行处理,所以在处理client1的时候,其他client处于等待状态。
这里有一个关键的点需要注意一下,其实redis在处理每个client的时候,最耗时的操作是通过系统内核与外部client进行通信,通信时需要与client建立连接,等待数据到达(或等待client处于可以接收写入数据的状态),当有数据到达时(或可写状态ready时),redis才对client进行读和写通信。所以,最漫长的是等待client状态是否可读可写。
其他的像读取到client的输入的数据,然后进行命令解析、数据查询、更新等操作都是在内存中执行的,对于单线程模型来说,这些都不是性能提升点(要想提升,感觉只能通过使用多线程来处理😂)。
当以IO复用的方式进行处理的时候:
上述示例,当通过IO复用的方式进行的话,整体的处理逻辑会相对有些变化,整体的执行性能也会有所提升,这都仰仗于操作系统提供的IO复用函数。注意,这里提到IO复用方式会提高性能,而性能的瓶颈在于redis通过系统内核与外部client进行的IO通信。
能使用IO复用的前提是操作系统提供了IO复用函数,不同操作系统所提供的IO复用函数api不一样,但是redis对每个IO复用函数都进行封装并对内统一的API,如下所示。
当使用IO复用的方式处理client的读写请求的时候,redis内部同样会存储一个client队列,不同在于,redis会通过系统内核提供的api,将这些client注册到操作系统内核中的一个队列中,操作系统的处理逻辑会帮助完成等待client可读可写状态是否ready的操作,当调用操作提供的一个获取就绪IO接口的时候,操作系统会返回一个队列,队列中的client都是处于已就绪状态,即可以直接对其进行读写操作。
即,使用IO复用的方式省去了redis自己去等待client是否就绪,而是由操作系统“帮你来等”。
6. 看下redis的文件事件是怎么执行的
有了上述一些背景知识的介绍,接下来我们来看下redis处理文件事件的代码是如何写的。
注册事件
由之前的叙述可知,通过IO复用技术对IO操作进行处理时,首先需要对IO事件进行注册以便操作系统进行监听。我们先看下redis中是如何对IO事件进行注册的。
在“Redis启动”部分讲解到,在main函数主体中通过调用一些函数来对RedisServer结构体进行初始化,其中有一个initServer()函数,如下所示:
void initServer() {
//此处省略很多。。。
/* Create an event handler for accepting new connections in TCP and Unix
* domain sockets. */
// 为 TCP 连接关联连接应答(accept)处理器
// 用于接受并应答客户端的 connect() 调用
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR)
{
redisPanic(
"Unrecoverable error creating server.ipfd file event.");
}
}
//此处省略很多。。。
}
如上代码省略了很多,只保留了初始化文件事件的逻辑。其中aeCreateFileEvent()函数的作用是创建一个文件事件,具体见下。
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
if (fd >= eventLoop->setsize) return AE_ERR;
// 取出文件事件结构
aeFileEvent *fe = &eventLoop->events[fd];
// 监听指定 fd 的指定事件
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return AE_ERR;
// 设置文件事件类型,以及事件的处理器
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
// 私有数据
fe->clientData = clientData;
// 如果有需要,更新事件处理器的最大 fd
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}
在此对aeCreateFileEvent参数做一下简单介绍:eventLoop存在于RedisServer;fd是一个事件描述符,可以简单理解为它是一个事件的id;mask使用标识事件的类型(读或写);proc是一个指向事件处理函数的指针;clientData存储了一些client相关的数据。
aeCreateFileEvent方法执行逻辑是:
- 取出eventLoop中events数组中index=fd对应的文件事件;
- 通过aeApiAddEvent函数将该文件事件加入到监听列表中;
- 将参数中mask设置为该文件事件的mask;
- 将参数中的proc设置为该文件事件的读或写处理函数;
- 将参数中的clientData设置为该文件事件的clientDate;
其中的2、4 步骤较为关键,步骤2是中的aeApiAddEvent函数是redis对各种底层操作系统多路复用函数的统一封装,在不同的操作系统下其内部调用的函数和执行逻辑不一样。我在看源码时使用的macOS,因此底层通过的kqueue来实现。这块具体的实现细节就不展开讲了(因为我也不是完全精通,关于kqueue可以参考这个),回顾之间对IO复用函数的介绍,这里可以理解为这里就是把一个IO读写事件在操作系统中进行了注册。
4步骤是对文件事件结构体读、写处理器的赋值,还记得最开始梳理的执行流程图吗?里面最终调用的rfileProc和wfileProc就是在这里进行初始化的。
在initServer()阶段,这里的proc是acceptTcpHandler函数,并且是一个读事件。
关于acceptTcpHandler函数的逻辑,稍后接着分析。现在先总结一下,在redis中通过aeCreateFileEvent来对文件事件进行事件类型(mask)和事件处理器的初始化,并将对应的事件添加到操作系统监听队列中。
回顾上面的逻辑,redis在初始化阶段就调用了aeCreateFileEvent来对文件事件初始化,在初始化逻辑中通过eventLoop->events[fd]的方式取出了文件事件结构,也就是在这之前就eventLoop->events中已经存在一些文件事件,对eventLoop->events的具体初始化逻辑也在initServer()函数中,这里就不展开讲了。不过需要指名的一点事,初始化的这个文件事件是一个监听socket连接的事件,即,此时将一个监听socket连接的读事件注册到操作系统。
acceptTcpHandler函数--连接应答处理器
刚才讲到,在第一次初始化文件事件的时候,给该文件事件绑定的是一个acceptTcpHandler函数,其函数处理逻辑如其名一样,即,对接收到的tcp连接时进行一些处理,如下所示。
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
// 此处省略很多。。。
while(max--) {
// accept 客户端连接
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (errno != EWOULDBLOCK)
redisLog(REDIS_WARNING,
"Accepting client connection: %s", server.neterr);
return;
}
redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
// 为客户端创建客户端状态(redisClient)
acceptCommonHandler(cfd,0);
}
}
上述展示了acceptTcpHandler函数的一些执行逻辑,这里我们重点看一下acceptCommonHandler函数,如下所示:
static void acceptCommonHandler(int fd, int flags) {
// 创建客户端
redisClient *c;
if ((c = createClient(fd)) == NULL) {
redisLog(REDIS_WARNING,
"Error registering fd event for the new client: %s (fd=%d)",
strerror(errno),fd);
close(fd); /* May be already closed, just ignore errors */
return;
}
//此处省略很多。。。
}
以上展示了acceptCommonHandler部分源码,整个函数的主要逻辑是创建一个redisClient,具体创建的方式在createClient函数中,如下所示:
redisClient *createClient(int fd) {
// 分配空间
redisClient *c = zmalloc(sizeof(redisClient));
//此处省略很多
// 需要用到这种伪终端
if (fd != -1) {
// 非阻塞
anetNonBlock(NULL,fd);
// 禁用 Nagle 算法
anetEnableTcpNoDelay(NULL,fd);
// 设置 keep alive
if (server.tcpkeepalive)
anetKeepAlive(NULL,fd,server.tcpkeepalive);
// 绑定读事件到事件 loop (开始接收命令请求)
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
}
// 此处省略很多。。。。
// 返回客户端
return c;
}
在createClient函数中主要是对RedisClient结构体实例的一些初始化动作,其他细节逻辑在此就不关注了,我们主要看下在这个函数逻辑中同样调用了aeCreateFileEvent函数。这个函数的两个关键的作用是:1. 向系统内核注册需要监听的读、写事件 2. 根据mask参数初始化文件事件处理器对应的读函数或写函数。
也就是说,此时redis向操作系统注册了一个fd对应的文件事件,并且对应的处理函数是readQueryFromClient。
readQueryFromClient函数--命令请求处理器
readQueryFromClient函数的执行逻辑很复杂,主要的目的是读取客户端输入的命令,并执行并将执行结果暂存到内存缓存中,并对文件事件绑定一个命令回复处理器函数。这里只对绑定命令回复处理器的函数调用链路进行一下梳理:
readQueryFromClient()-->processInputBuffer()-->processCommand()-->addReply()-->prepareClientToWrite()
int prepareClientToWrite(redisClient *c) {
// LUA 脚本环境所使用的伪客户端总是可写的
if (c->flags & REDIS_LUA_CLIENT) return REDIS_OK;
// 客户端是主服务器并且不接受查询,
// 那么它是不可写的,出错
if ((c->flags & REDIS_MASTER) &&
!(c->flags & REDIS_MASTER_FORCE_REPLY)) return REDIS_ERR;
// 无连接的伪客户端总是不可写的
if (c->fd <= 0) return REDIS_ERR; /* Fake client */
// 一般情况,为客户端套接字安装写处理器到事件循环
if (c->bufpos == 0 && listLength(c->reply) == 0 &&
(c->replstate == REDIS_REPL_NONE ||
c->replstate == REDIS_REPL_ONLINE) &&
aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
sendReplyToClient, c) == AE_ERR) return REDIS_ERR;
return REDIS_OK;
}
上述展示了prepareClientToWrite方法的处理逻辑,在方法最后的那个if中,我们又看到了熟悉的aeCreateFileEvent函数,这次绑定的处理器函数是sendReplyToClient(即,命令回复处理器),且将文件事件的类型设为写事件,并将该文件事件向操作系统进行注册。
至此,我们对各个文件事件处理器的绑定逻辑已进行了整体的描述。
- 系统启动时,第一次调用aeCreateFileEvent,此时对文件事件绑定acceptTcpHandler处理器;
- acceptTcpHandler在执行时,对文件事件绑定readQueryFromClient处理器;
- readQueryFromClient在执行时,对文件事件绑定sendReplyToClient处理器;
- sendReplyToClient执行结束后,对该文件事件的写事件从系统监听列表中删除。
再看一下aeProcessEvents --> aeApiPoll(eventLoop, tvp)
我们再来回顾一下aeProcessEvents中文件事件的执行逻辑
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* Nothing to do? return ASAP */
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
//文件事件执行开始
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
//这里省略了tvp获取的逻辑。。。。。
// 处理文件事件,阻塞时间由 tvp 决定
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
// 从已就绪数组中获取事件
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;
// 读事件
if (fe->mask & mask & AE_READABLE) {
// rfired 确保读/写事件只能执行其中一个
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
// 写事件
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
processed++;
}
}
//文件事件执行结束
// 执行时间事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
/* return the number of processed file/time events */
return processed;
}
该函数的执行逻辑中通过aeApiPoll函数获取已就绪的文件事件数量numevents,此时已就绪的文件事件已经存储在eventLoop->fired队列中,因此通过 aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd]便可获取events中已就绪的文件事件,并进行后续的执行逻辑。
7. 梳理一下整个执行过程
在此,我们对整个文件事件的执行过程进行一下梳理。
A. 服务启动初始化之eventLoop->events初始化:
此时,主要对eventLoop->events进行初始化,其中mask和读写处理器还未初始化,如下所示
B. 服务启动初始化之第一次执行aeCreateFileEvent
此时,对每个aeFileEvent的mask设置为读事件,并将读事件处理器设置为连接应答处理器(acceptTcpHandler),并将文件事件列表通过操作系统多路复用api添加到添加队列。
C. 服务启动初始化结束,开始无限循环处理文件事件,此时有客户端向服务器端发起连接
当监听到有客户端连接后,通过aeApiPoll方法获取到该就绪的文件事件,并执行对应的acceptTcpHandler处理器函数
D. acceptTcpHandler执行
当acceptTcpHandler执行结束后,对该文件事件绑定命令请求处理器(readQueryFromClient),并继续加入到监听队列。
E. 当下一次循环执行时,client1对服务端发出命令
当下一个无限循环执行时,此时,操作系统监听到client1对redis服务端发出命令请求,并将该文件事件加入到就绪队列。redis执行逻辑中通过aeApiPoll方法获取到该就绪的文件事件,并执行对应的readQueryFromClient处理器函数,对客户端输入的命令进行读取解析和执行,并将执行结果缓存至内存缓存中,同时对该文件事件绑定命令回复处理器(sendReplyToClient),并对mask和wfileProc进行设置。
F. 当下一次循环执行时,client1处于可写状态
下一次循环执行时,此时client1处于写就绪状态,操作系统将该文件事件加入到就绪队列,redis执行逻辑中通过aeApiPoll方法获取到该就绪的文件事件,并执行对应的sendReplyToClient处理器函数,函数执行后,client端会收到服务端对其发出的信息,同时取消对该文件事件写事件的监控。
至此,我们通过模拟一个客户端操作的方式整体梳理了一下整个文件事件的执行流程。
三、写在最后
上述的内容我们对Redis文件事件的初始化过程以及执行流程进行了梳理,相信通过对这些内容的理解,对于“单线程模型”、“IO多路复用”、“命令请求处理器/连接应答处理器/命令回复处理器”等这样的词汇将会更加容易被理解。文末再来看下这张文件事件执行图,结合之前的代码梳理,相信此时对于整个Redis文件事件的模型理解会更加深入。