一、Redis事件机制概述
Redis 服务器是一个事件驱动程序,它主要处理如下两种事件:
文件事件:利用 /0 复用机制,监听 Scket 等文件描述符上发生的事件。这类事件主要由客户端(或其他 Redis 服务器)发送网络请求触发。
时间事件:定时触发的事件,负责完成 Redis 内部定时任务,如生成 RDB 文件、清除过期数据等。
Redis 利用I/O 复用机制实现网络通信。I/O 复用是一种高性能 I/O 模型,它可以利用单进程监听多个客户端连接,当某个连接状态发生变化(如可读、可写) 时,操作系统会发送事件(这些事件称为已就绪事件)通知进程处理该连接的数据。
Redis 事件机制的实现代码在 ae.h、ae.c 中,它实现了上层逻辑,负责控制进程,使其阻塞等待事件就绪或处理已就绪的事件,并为不同系统的I/O复用API定义了一致的 Redis API:
aeApiCreate:初始化 IO 复用机制的上下文环境。
aeApiAddEvent、aeApiDelEvent:添加或删除一个监听对象
aeApiPoll:阻塞进程,等待事件就绪或给定时间到期。
ae_select.c、ae_epoll.c、ae_evport.c、ae kqueue.c 是 Redis 针对不同系统 I/O 复用机制的适配代码,分别调用 select、epoll、evport、kqueue 实现了上述 Redis API,a.c 会在 Redis 服务启动时根据操作系统支持的 I/O 复用 API选择使用合适的适配代码。(将ae.h、a.c 称为AE 抽象层,将ae _select.c、ae_epoll.c 等称为IO复用层)
//aeEventLoop 是 Redis 中的事件循环器,负责管理事件。
typedef struct aeEventLoop {
intmaxfd; //当前已注册的最大文件描述符。
int setsize; //该事件循环器允许监听的最大的文件描述符。
long long timeEventNextId; //下一个时间事件ID。
time_t lastTime; //上一次执行时间事件的时间,用于判断是否发生系统时钟偏移。
aeFileEvent *events; //已注册的文件事件表。
aeFiredEvent *fired; //已就绪的事件表。
aeTimeEvent *timeEventHead; //时间事件表的头节点指针。
int stop; //事件循环器是否停止。
void *apidata; //存放用于I/ 复用层的附加数据。
aeBeforeSleepProc *beforesleep; //进程阻塞前后调用的钩子函数
aeBeforeSleepProc *aftersleep; //进程阻塞前后调用的钩子函数
int flags;
} aeEventLoop;
//aeFileEvent存储了一个文件描述符上已注册的文件事件:
typedef struct aeFileEvent {
int mask; // 监听的文件事件类型,有以下值:AB_NONE、AE_READABLE、AB_WRITABLE。
aeFileProc *rfileProc; //AE READABLE 事件处理函数
aeFileProc*wfileProc; //AEWRITABLE 事件处理函数
void *clientData; //附加数据
} aeFileEvent;
aeFileEvent中并没有记录文件描述符fd的属性。POSIX标准对文件描述符fd有以下约束:
值为0、1、2的文件描述符分别表示标准输入、标准输出和错误输出。
每次新打开的文件描述符,必须使用当前进程中最小可用的文件描述符。
Redis充分利用文件描述符的这些特点,定义了一个数组aeEventLoop.events来存储已注的文件事件。数组索引即文件描述符,数组元素即该文件描述符上注册的文件事件,如aeEventLoop.events[99]存放了值为99的文件描述符的文件事件aeFileEvent。当该文件描述符发生了aeFileEventmask 指定的事件时,Redis 将调用 aeFileEvent.rfileProc 或aeFileEvent.wfilePro函数处理事件。
I0复用层会将已就绪的事件转化为aeFiredEvent,存放在aeEventLoopfired 中,等待事件循环器处理。
typedef struct aeFiredEvent {
int fd; // 产生事件的文件描述符
int mask; //产生的事件类型。
) aeFiredEvent;
// aeTimeEvent中存储了一个时间事件的信息:
typedef struct aeTimeEvent {
long long id; //时间事件的ID。
long when_sec; //时间事件下一次执行的秒数(UNIX 时间戳)和剩余毫秒数
long when_ms; //时间事件下一次执行的秒数(UNIX 时间戳)和剩余毫秒数
aeTimeProc*timeProc; //时间事件处理函数。
aeEventFinalizerProc *finalizerProc; //时间事件终结函数。
void *clientData; //客户端传入的附加数据。
struct aeTimeEvent *prev; //指向前一个时间事件
struct aeTimeEvent *next; //指向后一个时间事件
int refcount;
} aeTimeEvent;
二、Redis启动时创建的事件
Redis启动时,initServer 函数调用aeCreateEventLoop 函数创建一个事件循环器,存储在
server.el 属性中。
[1]、初始化aeEventLoop 属性。
[2]、aeApiCreate 由I/O 复用层实现,这时 Redis 已经根据运行系统选择了具体的IO复用层适配代码,该函数会调用到 ae_select.c、ae_epoll.c、ae_evport.c、ae_kqueue.c 其中的一个实现并初始化具体的IO 复用机制执行的上下文环境。
Redis 启动时,调用 aeCreateFileEvent 函数为 TCP Socket 等文件描述符注册了监听AE_WRITABLE类型的文件事件。所以,事件循环器会监听 TCP Socket,并使用指定函数处理AE_WRITABLE 事件。
/**
* fd:需监听的文件描述符
* mask:监听事件类型
* proc:事件处理函数
* clientData:附加数据
*/
int aeCreateFileEvent(aeEventLoop *eventLoop,int fd,int mask,
aeFileProc*proc,void *clientData)
{
// [1]
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
aeFileEvent *fe = &eventLoop->events[fd];
// [2]
if (aeApiAddEvent(eventLoop,fd,mask)== -1)
return AE ERR;
// [3]
fe->mask|= mask;
if(mask & AE READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
fe->clientData= clientData;
if(fd>eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}
serverCron 时间事件非常重要,负责完成 Redis 中的大部分内部任务,如定时持久化数据、清除过期数据、清除过期客户端等。另一部分内部任务则在 beforeSleep 函数中触发(事件循器每次阻塞前都调用的钩子函数)。Redis启动的最后,调用aeMain函数,启动事件循环器
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop =0;
while (!eventLoop->stop){
aeProcessEvents(eventLoop,AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|AE_CALL_AFTER_SLEEP);
}
}
只要不是 stop 状态,while 循环就一直执行下去,调用 aeProcessEvents 函数处理事件。Redis是一个事件驱动程序,正是该事件循环器驱动 Redis 运行并提供服务。
三、事件循环器的运行
Redis 运行期间,aeProcessEvents 函数被不断循环调用,处理 Redis 中的事件。
/**
* flags: 指定aeProcessEvents 函数处理的事件类型和事件处理策略
* AE_ALL_EVENTS:处理所有事件。
* AE_FILE_EVENTS:处理文件事件。
* AE_TIME_EVENTS:处理时间事件
* AE_DONT_WAIT:是否阻塞进程
* AE_CALL_AFTER_SLEEP:阻塞后是否调用eventLoopaftersleep 函数。
* AE_CALL_BEFORE_SLEEP:阻塞前是否调用eventLoop.beforesleep 函数
*/
int aeProcessEvents(aeEventLoop *eventLoop,int flags)
{
int processed = 0,numevents;
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE EVENTS)) return 0;
// [1]
if (eventLoop->maxfd != -1||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))){
int j;
// [2]
aeTimeEvent*shortest = NULL;
struct timeval tv,*tvp;
if (flags & AE_TIME_EVENTS && !(flags & AE DONT_WAIT))
shortest= aeSearchNearestTimer(eventLoop);
...
//[3]
if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)
eventLoop->beforesleep(eventLoop);
//[4]
numevents = aeApiPoll(eventLoop,tvp);
//[5]
if(eventLoop->aftersleep != NULL && flags & AE CALL AFTER SLEEP)
eventLoop->aftersleep(eventLoop);
// more
}
}
aeProcessEvents 函数执行的步骤流程:
[1]、判断是否需要阻塞进程。
[2]、按以下规则计算进程最大阻塞时间。
查找最先执行的时间事件,如果能找到,则将该事件执行时间减去当前时间作为进程的最大阻塞时间。
找不到时间事件,检查 flags 参数中是否 AE_DONT_WAT 标志,若不存在,则进程将一直阻塞,直到有文件事件就绪:若存在,则进程不阻塞,将不断询问系统是否有已就绪文件事件。另外,如果 evenLoop.ags 中存在 AE_DONT_WAIT 标志,那么进程也不会阻塞。
[3]、进程阻塞前,执行钩子函数 beforeSleep。
[4]、aeApiPoll 函数由 /0 复用层实现,负责阻塞当前进程,直到有文件事件就绪或者给定时间到期。该函数返回已就绪文件事件的数量,并将这些事件存储在 aeEventLoop.fired 中。
[5]、进程阻塞后,执行钩子函数 aftersleep。
由于 Redis 只有一个处理函数为serverCron 的时间事件,这里进程的最大阻塞时间为 serverCron时间事件的下次执行时间。
int aeProcessEvents(aeEventLoop *eventLoop,int flags)
{
...
// [6]
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 fired = 0;
// [7]
int invert = fe->mask & AE BARRIER;
if (!invert && fe->mask & mask & AE_READABLE){
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
fe = &eventLoop->events[fd];
}
// [8]
if (fe->mask & mask & AE_WRITABLE) {
if (!fired|| fe->wfileProc != fe->rfileProc){
fe->wf1leProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
// [9]
if (invert){
fe= &eventLoop->events[fd];
if((fe->mask & mask & AE READABLE) &&
(!fired|| fe->wfileProc != fe->rfileProc))
{
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
processed++;
}
}
// [10]
if (flags & AETIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed;
}
[6]、aeApiPoll 函数返回已就绪的文件事件数量,这里处理所有已就绪的文件事件。
[7]、如果就绪的是AE_READABLE 事件,则调用rfileProc 函数处理。通常 Redis 先处理AE_READABLE 事件,再处理AE_WRITABLE 事件,这有助于服务器尽快处理请求并回复结果给客户端。如果aeFileEventmask 中设置了AE_BARRIER标志,则优先处理 AE_WRITABLE事件。AEWRITABLE是 Redis 中预留的功能,Redis 中并没有使用该标志
[8]、如果就绪的是AE_WRITABLE 事件,则调用wileProc 函数处理
[9]、如果aeFileEventmask 中设置了AE_BARRIER标志,则在这里处理AE_READABLE事件。
[10]、processTimeEvents 函数处理时间事件。
上一次执行事件的时间比当前时间还大,说明系统时间混乱了 (由于系统时钟偏移等原因)。这里将所有时间事件 when sec 设置为 0,这样会导致时间事件提前执行,由于提前执行事件的危害比延后执行的小,所以 Redis 执行了该操作。
遍历时间事件。
aeTimeEventid 等于AE_DELETED_EVENT_ID,代表该时间事件已删除,将其从链
如果时间事件已到达执行时间,则执行aeTimeEvent.timeProc 函数。该函数执行时间表中移除。事件的逻辑并返回事件下次执行的间隔时间。事件下次执行间隔时间等于AE_NOMORE,代表该事件需删除,将aeTimeEventid 置为AE_DELETED_EVENT_ID,以便processTimeEvents函数下次执行时将其删除。由于 Redis 中只有 serverCron 时间事件,所以这里直接遍历所有时间事件也不会有性能
处理下一个时间事件。
由于 Redis 中只有 serverCron 时间事件,所以这里直接遍历所有时间事件也不会有性能问题。
另外,Redis 提供了 hz 配置项,代表 serverCron 时间事件的每秒执行次数,默认为 10,即
每隔100毫秒执行一次 serverCron 时间事件。
四、总结:
Redis 采用事件驱动机制,即通过一个死循环,不断处理服务中发生的事件。
Redis 事件机制可以处理文件事件和时间事件。文件事件由系统 IO 复用机制产生,通常由客户端请求触发。时间事件定时触发,负责定时执行 Redis 内部任务。
Redis事件机制执行流程如图所示:
