写在文章开头
redis通过单线程结合非阻塞事件轮询机制实现高效的网络IO和时间事件处理,这篇文章我们将从源码的角度深入分析一下redis时间事件的设计与实现。
Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
详解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) {//重写aofsize_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;}//......
}
小结
以上便是笔者从源码角度对于redis时间事件设计与实现的全部分析,希望对你有帮助。
我是 sharkchili ,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
参考
《redis设计与实现》