事件
本章介绍了 Redis 驱动的两大事件, 文件事件 和 时间事件.
文件事件(file event): Redis 通过套接字与客户端进行连接,而文件时间就是服务器对套接字的抽象.服务器与客户端的通信会产生相应的文件时间,而服务器则通过监听并处理这些时间来完成一系列网络通信操作.
时间时间(time event): Redis 服务器中的一些操作(serverCron) 需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象.
12.1 文件事件
Redis 基于 reactor 模式,开发了网络事件处理器,这个处理器被称为事件处理器:
1): 文件事件处理器使用 I/O 多路复用来同时监听多个套接字,并根据套接字目前执行的任务为套接字关联不同的事件处理器.
2): 当被监听的套接字准备好 连接应答(accept),读取(read),写入(write),关闭(close)等操作时,与操作相对应的文件事件就会产生,此时就用之前关联好的事件处理器进行对应的处理.
所以,虽然 Redis 是一个单线程程序,但是通过 I/O复用技术 在保持 Redis 内部单线程设计简单性的前提下,保证了高性能的网络通信模型.
12.1.1 文件事件的处理器构成
// 处理文件事件,阻塞时间由 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++;
}
其中 rfileProc 和 wfileProc 都是函数指针
该段代码位于主循环中的时间处理函数中,可以看到是一个经典的 Reactor 的一个设计, aeApiPoll 作为一个中间层,屏蔽了下层的实现,获取当前活动的事件,通过一个遍历进行一个执行操作.
而 rfileProc 和 wfileProc 是通过 aeCreateFileEvent 进行处理器的一个指定.
在确定事件时,也同时确定处理器的指向,保留有更大的修改空间
12.1.2 I/O 多路复用程序的实现
/* Include the best multiplexing layer supported by this system.
* The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
同过头文件声明来进行一个跨平台的实现,这也是一个经典做法.
该方法也是利用了编译规则来达到的.
include 本质就是将代码进行一个引入的过程,而函数的声明也是可以跟函数的定义分别进行一个编写,编译的时候,会先找到函数声明,再找到其定义,再进行一个链接,所以,当 include 的文件不同时,当然就会链接到不同的实现,从而实现跨平台的效果.
回到代码中, ae_evport ae_epoll.c ae_kqueue.c ae_select.c 分别由性能高低来进行一个头文件的优先级区分.
12.1.3 事件的类型
事件分为两大类,分别为可读事件和可写事件,
在可读事件中又可以分为客户端对服务器的 write 操作, close 操作, connect 操作, 套接字产生 AE_READABLE 事件
在可写事件中,套接字产生 AE_WRITABLE 事件.
12.1.4 API
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData)
将套接字进行一个监听行为加入 IO复用范围内,并将事件和事件处理器进行一个关联处理
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)
与一个 aeCreateFileEvent 是一个相反的函数, 进行一个从 Reactor 中移除,并取消事件和事件处理器的一个关联处理
int aeGetFileEvents(aeEventLoop *eventLoop, int fd)
获取被监听的事件类型
int aeWait(int fd, int mask, long long milliseconds)
阻塞 milliseconds 这么久的一个时间,等待并产生事件,时间通过 mask 传入
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
根据头文件 include 不同,有不同的实现,具体的行为是从 反应堆中 找出活跃的文件描述符.
int aeProcessEvents(aeEventLoop *eventLoop, int flags);
进行事件处理行为, aeApiPoll 的上层调用
char *aeGetApiName(void);
获取当前用的那种 IO复用库的名字.
12.1.5 文件事件的处理器
1). 连接应答处理器
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask);
当服务器进行初始化时,会将该函数与IO复用的监听套接字进行保定,如果监听套接字接受到客户端的 connect 函数的调用信息时,就执行该函数的内容
2): 命令请求处理器
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask)
为 socket 中 read 的包装,
当客户端已经与服务器建立连接之后,再发送命令请求时,就会产生 AE_READABBLE 事件,从而调用该函数,进行后续的时间处理
3): 命令恢复处理器
void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask);
在执行请求处理器后,该处理器会将得到的名句进行一个回复给客户端的一个行为,当回复结束时,会进行一个处理器和 AE_WRITEABLE 的一个解绑行为.
4): 一次完成的客户端与服务器连接事件示例
服务器监听套接字绑定 AE_READABLE,当连入时,触发监听套接字的连接应答处理器,生成一个客户端对象,绑定 fd,加入IO复用,开通 AE_READABLE 事件,使得客户端可以和服务器进行一个对话,当客户端发送一次命令后,客户端对象将开通 AE_WRITEABLE 事件,服务器进行一个回复之后,再解除事件.
12.2 时间事件
Redis 的时间事件分为以下两类:
定时事件,在间隔一段事件后执行一次
周期性事件, 每间隔一段时间就执行一次
typedef struct aeTimeEvent {
// 时间事件的唯一标识符
long long id; /* time event identifier. */
// 事件的到达时间
long when_sec; /* seconds */
long when_ms; /* milliseconds */
// 事件处理函数
aeTimeProc *timeProc;
// 事件释放函数
aeEventFinalizerProc *finalizerProc;
// 多路复用库的私有数据
void *clientData;
// 指向下个时间事件结构,形成链表
struct aeTimeEvent *next;
} aeTimeEvent;
时间事件结构如上.
是否为周期事件,取决于事件处理器是否返回了 AE_NOMORE 关键字.
书中的版本 Redis 只使用周期性事件,没有使用定时事件
12.2.1 实现
Redis 使用的链表进行一个定时事件的串联,每次循环从头到尾进行一个遍历.比较简单粗暴…
12.2.2 API
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc)
创建一个定时事件
int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id);
通过创建时给的 id 进行删除
static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop);
返回最近的一个时间事件
static int processTimeEvents(aeEventLoop *eventLoop)
时间事件的执行器,用来遍历所有的时间事件
12.2.3 时间事件应用实例 serverCron 函数
/* Create the serverCron() time event, that's our main way to process
* background operations. */
// 为 serverCron() 创建时间事件
if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
redisPanic("Can't create the serverCron time event.");
exit(1);
}
可以看到 severCron 的启动就是通过时间事件运行的
12.3 事件的调度与执行
/*
* 事件处理器的主循环
*/
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件处理前执行的函数,那么运行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 开始处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
在主循环中进行 aeProcessEvents 的一个执行,直到服务器的结束.
需要注意的是,时间事件可能晚于预设的时间,在时间事件未到达时,可能执行了读写事件,占用了较长的时间,所以可能会出现不精准的情况
12.4 总结
Redis 是一个事件驱动程序, 事件分为 时间事件和文件事件 两类
文件事件处理器是通过 Rector 模式实现的网络通信程序
文件事件是对套接字操作的抽象,每次套接字变为 可应答,可写,可读时,对应的文件事件就会产生
文件事件总体上 分为 AE_READABLE 和 AE_WIRETEABLE 两类
时间事件分为定时事件和周期事件,定时时间只在指定的时间到达一次,周期就是间隔一点时间进行调用
服务器一般只执行 serverCron 周期事件
文件事件和时间事件是合作关系,服务器会进行一个轮流处理
时间事件的实际处理时间可能会更晚一点