1. 事件
Redis服务器是一个事件驱动程序,服务器需要处理以下两种事件:
- 文件事件(file event):Redis服务器通过套接字与客户端(或其他Redis服务器)连接,而
文件事件就是服务器对套接字操作的抽象
。服务器与客户端(或其他Redis服务器)的通信会产生相应的文件事件
,而服务器则通过监听并处理这些事件来完成一系列网络通信操作
。 - 时间时间(time event):Redis服务器中的一些操作需要在给定的时间点执行,而时间事件就是Redis对这类操作的抽象。
2. 文件事件处理器
Redis基于Reactor模式
开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler
)。
- 文件事件处理器使用
I/O多路复用(multiplexing)
程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。 - 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与之相对应的
文件事件就会产生
,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件
。
虽然Redis中的文件事件处理器是以单线程的形式运行,但是通过I/O多路复用程序来监听多个套接字,实现了高性能的网络通信模型。同时还可以和Redis中其他单线程模块进行对接,保证了内部设计的简单性。
如下图所示,文件事件处理器包含了下面四个组成部分:
- 套接字:可以看做是客户端和服务器的链接对象;
- I/O多路复用程序:负责监听多个套接字,并向文件事件分派器
传递产生了事件的套接字
; - 文件事件分派器(dispatcher):接收I/O多路复用程序传递过来的套接字,并
根据套接字产生的事件类型调用对应的事件处理器
; - 事件处理器:其实就是一个个的函数,
不同类型的套接字事件会调用不同的函数
。
多个套接字可能会并发产生许多文件事件,而I/O多路复用程序会将这些产生事件的套接字全部统一放到一个队列里去
,然后通过这个队列,以有序、同步、每次获取一个套接字的方式传递给文件事件分派器
。只有当前一个套接字产生的文件事件被处理完毕之后(关联的事件处理器执行完毕),I/O多路复用程序才会向文件事件分派器传递下一个套接字
。我们常说的Redis是单线程的模型就是这个意思。如下图所示:
3. 事件处理器的类型
Redis为不同的文件事件编写了对应的处理器,分别用于实现不同的网络通信需求,例如:
- 连接应答处理器:为了对连接服务器的各个客户端进行应答;
- 命令请求处理器:为了接受客户端传来的命令请求;
- 命令回复处理器:为了向客户端返回命令的执行结果;
- 复制处理器:主从服务器进行复制操作时,都需要关联对应的复制处理器执行复制功能。
当然还包括其他的事件处理器,最常用的就是连接应答处理器、命令请求处理器和命令回复处理器。
4. 一次完整的客户端与服务器连接事件的实例:
(1)Redis服务器启动之后,文件事件处理器中的I/O多路复用程序会监听AE_READABLE事件
,也就是有套接字连接服务器的时候,就会产生这个事件;
(2)假设有一个客户端连接了服务器,此时就会产生AE_READABLE事件,I/O多路复用程序会将这个套接字传递给事件分派器
;
(3)事件分派器为这个套接字关联一个连接应答处理器
,然后应答处理器会负责和客户端建立连接
。接着建立连接完成之后会解除AE_READABLE事件和连接应答处理器的关联
;
(4)假设一个已连接的客户端发送了一个命令请求,那么客户端套接字也会产生AE_READABLE事件
,但是此时时间分派器会关联一个命令请求处理器给这个事件
,然后命令请求处理器会读取客户端的命令内容,再传给相应的程序执行
。接着读取完成之后会解除AE_READABLE事件和命令请求处理器的关联
;
(5)当客户端尝试读取命令回复的时候,会产生AE_WRITEABLE事件
,然后事件分派器会关联一个命令回复处理器给这个事件,命令回复处理器会将之前的命令执行结果写入套接字
。然后写入完成之后会解除AE_WRITEABLE事件和命令回复处理器的关联
。
5. 事件的调度与执行
Redis 服务器中同时存在文件事件和时间事件两种事件类型,所以服务器必须对这两种类型的事件进行调度,决定何时处理文件事件,何时处理时间事件。事件调度和执行由 ae.c/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;
/* Note that we want call select() even if there are no
* file events to process as long as we want to process time
* events, in order to sleep until the next time event is ready
* to fire. */
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp;
// 获取最近的时间事件
if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
shortest = aeSearchNearestTimer(eventLoop);
if (shortest) {
// 如果时间事件存在的话
// 那么根据最近可执行时间事件和现在时间的时间差来决定文件事件的阻塞时间
long now_sec, now_ms;
/* Calculate the time missing for the nearest
* timer to fire. */
// 计算距今最近的时间事件还要多久才能达到
// 并将该时间距保存在 tv 结构中
aeGetTime(&now_sec, &now_ms);
tvp = &tv;
tvp->tv_sec = shortest->when_sec - now_sec;
if (shortest->when_ms < now_ms) {
tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
tvp->tv_sec --;
} else {
tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
}
// 时间差小于 0 ,说明事件已经可以执行了,将秒和毫秒设为 0 (不阻塞)
if (tvp->tv_sec < 0) tvp->tv_sec = 0;
if (tvp->tv_usec < 0) tvp->tv_usec = 0;
} else {
// 执行到这一步,说明没有时间事件
// 那么根据 AE_DONT_WAIT 是否设置来决定是否阻塞,以及阻塞的时间长度
/* If we have to check for events but need to return
* ASAP because of AE_DONT_WAIT we need to set the timeout
* to zero */
if (flags & AE_DONT_WAIT) {
// 设置文件事件不阻塞
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
/* Otherwise we can block */
// 文件事件可以阻塞直到有事件到达为止
tvp = NULL; /* wait forever */
}
}
// 处理文件事件,阻塞时间由 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;
/* note the fe->mask & mask & ... code: maybe an already processed
* event removed an element that fired and we still didn't
* processed, so we check if the event is still valid. */
// 读事件
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++;
}
}
/* Check time events */
// 执行时间事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed; /* return the number of processed file/time events */
}
具体流程如下:
(1) 计算最接近的时间时间还有多少毫秒ts达到,如果已经达到则为0;
(2)阻塞并等待文件事件的产生,最大阻塞时间就是上面计算得到的ts;
(3)如果有文件事件产生则退出阻塞并处理文件事件;
(4)所有的文件事件处理完成之后,再处理时间事件,如果没有时间达到,则继续循环到第一步。
事件处理角度下的Redis服务器运行流程:
THE END.