Redis的时间事件分为两类:
1、定时事件:让一段程序在指定的时间之后执行一次。
2、周期性事件:让一段程序每隔指定的时间就执行一次。(比如serverCron函数,每秒执行次数为server.hz)
目前版本的Redis只使用周期性事件,而没有使用定时事件。具体源码参考ae.c/ae.h文件中。文中的源码注释参考于黄建宏。
在介绍时间事件结构之前,先看表征事件处理器的状态的结构:
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;
时间事件结构:
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;
关键属性具体解释如下:
1、id:服务器为该时间事件创建的全局唯一标识号。新创建的时间事件的id号比旧事件大,依次递增加1。这一点可以从事件处理器的状态aeEventLoop中定义的变量timeEventNextId看出。
2、when_sec/when_ms:ms精度的UNIX时间域,记录了事件的到达时间。
3、timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会执行对应的实事件处理器,并获取返回值。
该返回值retval来决定是否需要循环执行这个时间事件,有两种情况:
(1)AE_NOMORE(-1):在ae.h中定义的常量值。表示该事件是一个定时事件,当该事件到达执行一次后,就会删除。
(2)具体的ms值:表示retval毫秒后继续执行该时间事件。
4、next:指向下一个时间事件,形成无序链表结构,该无序体现在并非按照到达时间进行排序。新的时间事件是插入在该链表的表头位置。
这里面有个问题:采用无序链表结构,遍历执行时会不会影响时间事件处理器的性能?
答:不会。目前版本中,正常模式中Redis服务器只使用serverCron一个时间事件,在benchmark模式下,也只用两个时间事件。在这种情况下,服务器几乎将无序链表退化成一个指针来使用,所以并不影响事件执行的性能。
常用的API:
1、aeCreateTimeEvent:添加新的事件到链表表头
/*
* 创建时间事件
* 返回时间事件的id
*/
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc)
{
// 更新时间计数器
long long id = eventLoop->timeEventNextId++;
// 创建时间事件结构
aeTimeEvent *te;
te = zmalloc(sizeof(*te));
if (te == NULL) return AE_ERR; //事件执行状态:出错
// 设置 ID
te->id = id;
// 设定处理事件的时间
aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
// 设置事件处理器
te->timeProc = proc;
//设置事件释放函数
te->finalizerProc = finalizerProc;
// 设置私有数据
te->clientData = clientData;
// 将新事件放入表头
te->next = eventLoop->timeEventHead;
eventLoop->timeEventHead = te;
return id;
}
设置到达时间的函数aeAddMillisecondsToNow源码为:
/*
* 在当前时间上加上 milliseconds 毫秒,
* 并且将加上之后的秒数和毫秒数分别保存在 sec 和 ms 指针中。
*/
static void aeAddMillisecondsToNow(long long milliseconds, long *sec, long *ms) {
long cur_sec, cur_ms, when_sec, when_ms;
// 获取当前时间
aeGetTime(&cur_sec, &cur_ms);
// 计算增加 milliseconds 之后的秒数和毫秒数
when_sec = cur_sec + milliseconds/1000;
when_ms = cur_ms + milliseconds%1000;
// 进位:
// 如果 when_ms 大于等于 1000
// 那么将 when_sec 增大一秒
if (when_ms >= 1000) {
when_sec ++;
when_ms -= 1000;
}
// 保存到指针中
*sec = when_sec;
*ms = when_ms;
}
2、aeDeleteTimeEvent:删除给定id的时间事件
int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id)
{
aeTimeEvent *te, *prev = NULL;
// 遍历链表
te = eventLoop->timeEventHead;
while(te) {
// 发现目标事件,删除
if (te->id == id) {
if (prev == NULL)
eventLoop->timeEventHead = te->next;
else
prev->next = te->next;
// 执行清理处理器
if (te->finalizerProc)
te->finalizerProc(eventLoop, te->clientData);
// 释放时间事件
zfree(te);
return AE_OK;
}
prev = te;
te = te->next;
}
return AE_ERR; /* NO event with the specified ID found */
}
3、aeSearchNearestTimer:寻找离目前时间最近的时间事件
// 寻找里目前时间最近的时间事件
// 因为链表是乱序的,所以查找复杂度为 O(N)
static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop)
{
aeTimeEvent *te = eventLoop->timeEventHead;
aeTimeEvent *nearest = NULL;
while(te) {
if (!nearest || te->when_sec < nearest->when_sec || //遍历头节点时nearest为空
(te->when_sec == nearest->when_sec &&
te->when_ms < nearest->when_ms))
nearest = te;
te = te->next;
}
return nearest;
}
4、processTimeEvents:处理所有已到达的时间事件
static int processTimeEvents(aeEventLoop *eventLoop) {
int processed = 0;
aeTimeEvent *te;
long long maxId;
time_t now = time(NULL);
/* If the system clock is moved to the future, and then set back to the
* right value, time events may be delayed in a random way. Often this
* means that scheduled operations will not be performed soon enough.
*
* Here we try to detect system clock skews, and force all the time
* events to be processed ASAP when this happens: the idea is that
* processing events earlier is less dangerous than delaying them
* indefinitely, and practice suggests it is. */
// 如果系统时钟被移动到未来,然后返回到正确的值,那么时间事件可能会以一种随机的方式延迟。通常这意味着计划的操作不会很快执行。
// 在这里,我们试图检测系统时钟的倾斜,并在发生这种情况时强制所有的时间事件被处理。
// 早期处理事件比无限期地延迟它们的危险要小
// 通过重置事件的运行时间,
// 防止因时间穿插(skew)而造成的事件处理混乱
if (now < eventLoop->lastTime) {
te = eventLoop->timeEventHead;
while(te) {
te->when_sec = 0;
te = te->next;
}
}
// 更新最后一次处理时间事件的时间
eventLoop->lastTime = now;
// 遍历链表
// 执行那些已经到达的事件
te = eventLoop->timeEventHead;
maxId = eventLoop->timeEventNextId-1;
while(te) {
long now_sec, now_ms;
long long id;
// 跳过无效事件
if (te->id > maxId) {
te = te->next;
continue;
}
// 获取当前时间
aeGetTime(&now_sec, &now_ms);
// 如果当前时间等于或等于事件的执行时间,那么说明事件已到达,执行这个事件
if (now_sec > te->when_sec ||
(now_sec == te->when_sec && now_ms >= te->when_ms))
{
int retval;
id = te->id;
// 执行事件处理器,并获取返回值
retval = te->timeProc(eventLoop, id, te->clientData);
processed++;
/* After an event is processed our time event list may
* no longer be the same, so we restart from head.
* Still we make sure to don't process events registered
* by event handlers itself in order to don't loop forever.
* To do so we saved the max ID we want to handle.
*
* FUTURE OPTIMIZATIONS:
* Note that this is NOT great algorithmically. Redis uses
* a single time event so it's not a problem but the right
* way to do this is to add the new elements on head, and
* to flag deleted elements in a special way for later
* deletion (putting references to the nodes to delete into
* another linked list). */
// 记录是否有需要循环执行这个事件时间
if (retval != AE_NOMORE) {
// 是的, retval 毫秒之后继续执行这个时间事件
aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
} else {
// 不,将这个事件删除
aeDeleteTimeEvent(eventLoop, id);
}
// 因为执行事件之后,事件列表可能已经被改变了
// 因此需要将 te 放回表头,继续开始执行事件
te = eventLoop->timeEventHead;
} else {
te = te->next;
}
}
return processed;
}
通过遍历时间事件链表,来处理所有已经到达的时间事件,需要注意的是:
(1)如果系统时钟被移动到未来,那么时间事件可能会以一种随机的方式延迟,意味着计划的操作不会很快被执行。所以需要用eventLoop中定义的lastTime来检测系统时钟的倾斜,确保发生这种情况时强制所有的时间事件被执行:te->when_sec=0。
(2)执行完具体的事件处理器后,根据返回值retval来判断是否需要循环执行这个时间事件。
(3)执行完具体的事件后,事件链表可能已经被改变了,因为需要重新迭代至表头,继续判断执行事件。
事件的调度与执行:
前面提过,Redis中的事件分为时间事件和文本事件,具体的调度源码为:
/* Process every pending time event, then every pending file event
* (that may be registered by time event callbacks just processed).
*
* 处理所有已到达的时间事件,以及所有已就绪的文件事件。
*
* Without special flags the function sleeps until some file event
* fires, or when the next time event occurs (if any).
*
* 如果不传入特殊 flags 的话,那么函数睡眠直到文件事件就绪,
* 或者下个时间事件到达(如果有的话)。
*
* If flags is 0, the function does nothing and returns.
* 如果 flags 为 0 ,那么函数不作动作,直接返回。
*
* if flags has AE_ALL_EVENTS set, all the kind of events are processed.
* 如果 flags 包含 AE_ALL_EVENTS ,所有类型的事件都会被处理。
*
* if flags has AE_FILE_EVENTS set, file events are processed.
* 如果 flags 包含 AE_FILE_EVENTS ,那么处理文件事件。
*
* if flags has AE_TIME_EVENTS set, time events are processed.
* 如果 flags 包含 AE_TIME_EVENTS ,那么处理时间事件。
*
* if flags has AE_DONT_WAIT set the function returns ASAP until all
* the events that's possible to process without to wait are processed.
* 如果 flags 包含 AE_DONT_WAIT ,
* 那么函数在处理完所有不许阻塞的事件之后,即刻返回。
*
* The function returns the number of events processed.
* 函数的返回值为已处理事件的数量
*/
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. */
// 请注意,我们想要调用select,尽管没有文件事件要处理,只要我们想处理时间事件,以便休眠,直到在下一次事件准备好被触发。
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 决定
// aeApiPoll() 调用了 select() 进入了监听轮询
// 返回已就绪的文件事件数量
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)输入参数中的flag是决定执行什么的事件。
0:什么也不做,直接返回
AE_FILE_EVENTS :1,只处理文件事件
AE_TIME_EVENTS :2,只处理时间事件
AE_ALL_EVENTS:3,先处理文件事件,再处理时间事件
AE_DONT_WAIT:4,不阻塞,也不进行等待
(2)寻找距离现在最近的时间事件,先处理文件事件,aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,该函数调用了select函数进入了监听轮询,返回已就绪的文件事件数量。接着,遍历执行相应的读事件或写事件(注意,读/写事件只能执行其中一个)。最后,执行时间事件。