Redis通过单线程结合非阻塞事件轮询机制实现高效的网络IO和时间事件处理,这篇文章我们将从源码的角度深入分析一下redis时间事件的设计与实现。
详解redis中的时间事件
时间事件的定义
时间事件可以是单次到期执行销毁,也可以是定时任务,对此redis对于时间事件统一封装为aeTimeEvent对象,通过id来唯一标识一个事件,结合when_sec和when_ms记录任务到期执行的秒和分,而执行时间事件的函数也是交由timeProc指针所指向的函数执行。 我们以一个redis定时执行的任务为例,如下所示,该结果通过when_sec和when_ms记录秒之前的时间和毫秒的时间,一旦这个时间到了就会执行timeProc这个函数指针所指向的方法serverCron,该函数会定期执行各种任务,这一点笔者会在后文展开:
对应的我们给出时间事件的代码描述,即位于ae.h这个头文件中的aeTimeEvent 结构体,这就是对时间事件的封装结构体,可以看到它除了笔者上述提到的核心字段以外,还有一个next指针用于连接下一个注册的时间事件:
上文提到redis的时间事件是以链表的形式关联起来,这里我们也给出时间时间统一管理对象,即时间轮询器aeEventLoop ,它通过timeEventHead记录第一个时间时间而后续的时间时间统一用时间时间的next指针进行管理:
对应我们也给出这段时间代码的定义,即位于ae.h中aeEventLoop 的定义:
注册时间事件
redis在服务器初始化阶段,会注册一个定时的时间事件,大约每1毫秒触发一次,该事件主要做的是:
- 更新redis全局时钟,该时钟用于全局变量获取时间用的。
- 随机抽取redis内存数据库中的样本删除过期的键值对。
- 如果检查到aof重写完成,则进行刷盘操作。
- 如果发现当前aof大小过大,则fork子进程进行aof重写操作。
- ......。
对应我们给出时间事件注册的源码段,即redis初始化时调用的方法initServer中的aeCreateTimeEvent,可以看到它将定时任务封装为时间事件timeEvent,并设置时间间隔为1毫秒一次:
轮询处理时间事件
redis每次处理完所有用户的请求之后,都会调用一次时间时间处理函数processTimeEvents,轮询并处理就绪的时间事件,由此保证尽可能准时执行时间事件,如果事件时间非定时任务则执行完成直接删除,反之设置下一次执行时间。这些步骤全部完成之后,返回本次处理的时间事件数:
我们给出处理时间循环的入口aeMain,可以看到该函数就是redis核心函数所在,它会循环调用aeProcessEvents处理各种事件:
不如aeProcessEvents可以看到该函数执行完所有用户请求之后调用processTimeEvents方法获取并执行就绪的时间事件:
最后我们就可以看到处理时间事件的核心代码段,其内部会从timeEventHead开始轮询就绪的时间事件,比对当前时间是否大于或者等于到期时间,如果是则执行当前时间事件,再判断这个事件是否是定时事件,如果是则更新下次执行时间,反之删除,最后累加本次处理的时间时间数:
redis对于时间事件实现上的优化
因为时间事件有些要求定期执行,所以redis为了保证时间执行的实时性,做了如下两个优化:
- 对于比较耗时的时间事件,例如AOF重写,通过fork子进程异步完成:
- 对于返回给客户端套接字的内容,如果长度超过预设的值,会主动让出线程执行权,避免时间时间饥饿。
对应的我们给出第一点时间时间对于aof重写的核心代码段,可以看到serverCron内部判断如果当前没有rdb和aof子进程,且需要进行aof重写则调用rewriteAppendOnlyFileBackground函数fork子进程进行aof重写:
而回复给客户端结果的处理器sendReplyToClient内部也有一段,判断如果写入数totwritten 大于REDIS_MAX_WRITE_PER_EVENT (宏定义为64M),则直接中止写入,break退出等到下一次循环处理,避免因为这个处理导致其他时间事件饥饿而导致事件执行延期: