Redis通过单线程结合非阻塞事件轮询机制实现高效的网络IO和时间事件处理,这篇文章我们将从源码的角度深入分析一下redis时间事件的设计与实现。
详解redis中的时间事件
时间事件的定义
时间事件可以是单次到期执行销毁,也可以是定时任务,对此redis对于时间事件统一封装为aeTimeEvent对象,通过id来唯一标识一个事件,结合when_sec和when_ms记录任务到期执行的秒和分,而执行时间事件的函数也是交由timeProc指针所指向的函数执行。 我们以一个redis定时执行的任务为例,如下所示,该结果通过when_sec和when_ms记录秒之前的时间和毫秒的时间,一旦这个时间到了就会执行timeProc这个函数指针所指向的方法serverCron,该函数会定期执行各种任务,这一点笔者会在后文展开:
对应的我们给出时间事件的代码描述,即位于ae.h这个头文件中的aeTimeEvent 结构体,这就是对时间事件的封装结构体,可以看到它除了笔者上述提到的核心字段以外,还有一个next指针用于连接下一个注册的时间事件:
//时间事件
typedef struct aeTimeEvent {
//时间事件的id全局递增
long long id; /* time event identifier. */
long when_sec; /* seconds */
//时间到达的时间
long when_ms; /* milliseconds */
//对应时间时间的处理器
aeTimeProc *timeProc;
//......
//连接下一个时间时间
struct aeTimeEvent *next;
} aeTimeEvent;
上文提到redis的时间事件是以链表的形式关联起来,这里我们也给出时间时间统一管理对象,即时间轮询器aeEventLoop ,它通过timeEventHead记录第一个时间时间而后续的时间时间统一用时间时间的next指针进行管理:
对应我们也给出这段时间代码的定义,即位于ae.h中aeEventLoop 的定义:
typedef struct aeEventLoop {
//......
//管理时间事件的列表
aeTimeEvent *timeEventHead;
//......
} aeEventLoop;
注册时间事件
redis在服务器初始化阶段,会注册一个定时的时间事件,大约每1毫秒触发一次,该事件主要做的是:
- 更新redis全局时钟,该时钟用于全局变量获取时间用的。
- 随机抽取redis内存数据库中的样本删除过期的键值对。
- 如果检查到aof重写完成,则进行刷盘操作。
- 如果发现当前aof大小过大,则fork子进程进行aof重写操作。
- ......。
对应我们给出时间事件注册的源码段,即redis初始化时调用的方法initServer中的aeCreateTimeEvent,可以看到它将定时任务封装为时间事件timeEvent,并设置时间间隔为1毫秒一次:
void initServer(void) {
//......
/* Create the serverCron() time event, that's our main way to process
* background operations. */
//创建时间事件注册到eventLoop->timeEventHead中
if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
redisPanic("Can't create the serverCron time event.");
exit(1);
}
//......
}
轮询处理时间事件
redis每次处理完所有用户的请求之后,都会调用一次时间时间处理函数processTimeEvents,轮询并处理就绪的时间事件,由此保证尽可能准时执行时间事件,如果事件时间非定时任务则执行完成直接删除,反之设置下一次执行时间。这些步骤全部完成之后,返回本次处理的时间事件数:
我们给出处理时间循环的入口aeMain,可以看到该函数就是redis核心函数所在,它会循环调用aeProcessEvents处理各种事件:
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
//处理各种事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
不如aeProcessEvents可以看到该函数执行完所有用户请求之后调用processTimeEvents方法获取并执行就绪的时间事件:
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
//......
//处理就绪的客户端事件
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 = 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++;
}
}
//上述核心网络IO事件完成后处理时间事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed; /* return the number of processed file/time events */
}
最后我们就可以看到处理时间事件的核心代码段,其内部会从timeEventHead开始轮询就绪的时间事件,比对当前时间是否大于或者等于到期时间,如果是则执行当前时间事件,再判断这个事件是否是定时事件,如果是则更新下次执行时间,反之删除,最后累加本次处理的时间时间数:
static int processTimeEvents(aeEventLoop *eventLoop) {
int processed = 0;
aeTimeEvent *te;
long long maxId;
time_t now = time(NULL);
//......
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++;
//.....
if (retval != AE_NOMORE) {//如果事件类型不是AE_NOMORE则说明是定时事件更新周期,反之删除
aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
} else {
aeDeleteTimeEvent(eventLoop, id);
}
te = eventLoop->timeEventHead;
} else {
te = te->next;
}
}
return processed;
}
redis对于时间事件实现上的优化
因为时间事件有些要求定期执行,所以redis为了保证时间执行的实时性,做了如下两个优化:
- 对于比较耗时的时间事件,例如AOF重写,通过fork子进程异步完成:
- 对于返回给客户端套接字的内容,如果长度超过预设的值,会主动让出线程执行权,避免时间时间饥饿。
对应的我们给出第一点时间时间对于aof重写的核心代码段,可以看到serverCron内部判断如果当前没有rdb和aof子进程,且需要进行aof重写则调用rewriteAppendOnlyFileBackground函数fork子进程进行aof重写:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
//......
/* Start a scheduled AOF rewrite if this was requested by the user while
* a BGSAVE was in progress. */
//aof_rewrite_scheduled设置为1,且没有其他持久化子进程则进行aof重写,通过异步避免耗时
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.aof_rewrite_scheduled)
{
rewriteAppendOnlyFileBackground();
}
//......
}
//fork子进程进行aof重写
int rewriteAppendOnlyFileBackground(void) {
//......
if ((childpid = fork()) == 0) {//fork子进程进行aof重写
char tmpfile[256];
/* Child */
closeListeningSockets(0);
redisSetProcTitle("redis-aof-rewrite");
//生成一个tmp文件
snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {//重写aof
size_t private_dirty = zmalloc_get_private_dirty();
//......
exitFromChild(0);
} else {
exitFromChild(1);
}
} else {
//......
}
return REDIS_OK; /* unreached */
}
而回复给客户端结果的处理器sendReplyToClient内部也有一段,判断如果写入数totwritten 大于REDIS_MAX_WRITE_PER_EVENT (宏定义为64M),则直接中止写入,break退出等到下一次循环处理,避免因为这个处理导致其他时间事件饥饿而导致事件执行延期:
void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
//......
while(c->bufpos > 0 || listLength(c->reply)) {
//......
//对于文件事件数据写入超长会让出执行权让时间事件能够尽可能的执行
server.stat_net_output_bytes += totwritten;
if (totwritten > REDIS_MAX_WRITE_PER_EVENT &&
(server.maxmemory == 0 ||
zmalloc_used_memory() < server.maxmemory)) break;
}
//......
}