一个简单的定时任务调度中心设计方案
在日常开发中除了给前端开发接口,还要写一些定时处理任务,比如一个活动需要每天定时给所有用户派发奖励。一个成熟服务框架需要一个全局的定时任务调度中心,通过定时任务调度中心可以查看服务有哪些定时任务以及定时任务的执行情况,对于执行失败的定时任务可以手动执行等。
我们公司的微服务架构没有定时任务调度中心,每个服务通过类似crontab
定时任务配置来管理自身的定时任务,为了保证每个服务高可用,我们给每个服务都部署了两个节点,对于一些不能并发执行的定时任务我们往往需要给定时任务加一个分布式锁,有时甚至需要修改服务的crontab
定时任务配置,保证一组服务只一个服务能执行定时任务,对于定时任务执行情况没有一个统一地方可以查看,需要查看每个服务日志才能确认定时任务是否执行成功,对于执行失败的定时任务需要写额外的重做代码。所以我们需要一个定时任务调度中心来管理每个服务的定时任务,这个定时任务调度中心需要有以下功能:
- 可以查看服务有哪些定时任务以及任务的执行状态。
- 对于执行失败的定时任务可以在管理界面手动发起重做。
- 每个定时任务都抽象成接口,由定时任务调度中心负责调度。
- 定时任务什么时候执行应由具体业务服务配置,定时任务调度中心可获取这些配置进行定时调度。
既然决定开发一个定时任务调度中心,那么问题来了,我们是否需要开发一个新的服务?我的答案是不需要,我们把定时任务当作服务接口,我们所有服务的接口信息都登记到服务注册中心,我们可以给接口添加一个定时任务标签,注册中心将打上定时任务标签的服务接口放到一个定时任务执行队列中,由定时任务执行队列管理每个服务定时任务接口的调度,同时将各个服务的定时任务接口与执行情况记录到数据库中,然后在注册中心添加一个定时任务管理页面来管理各个服务的定时任务。
定时任务调度中心的大致思路有了,剩下的过程就是将实现思路具体化。我们现有的定时任务配置与crontab
类似,可以按分钟、小时、日期、月份、星期等几个维度进行配置,如果定时任务调度中心全部实现这几个维度的配置,会导致定时任务配置解析与调度逻辑变得复杂,一个框架因为简单才更加稳定可靠,我决定对定时任务配置维度进行简化处理,最终抽象出以下两类定时任务:
- 每隔N秒执行一次的定时任务。
- 每天特定时间点执行的定时任务。
每个定时任务都可能执行多次,即每个定时任务可以有多条执行记录,所以可以在定时任务管理页面中查看每个定时任务的执行情况,但日常运维中我们不是很关心定时任务的历史执行情况,如果记录定时任务每次的执行情况,我们还要考虑如何管理历史执行记录以及历史执行记录太多了该如何处理,我不希望因为开发定时任务调度中心导致服务注册中心过于臃肿(定时任务调度中心当作服务注册中心的一个子模块来开发),所以我决定我们的定时任务调度中心只记录每个定时任务最近一次的执行记录。
对于日常运营活动中的定时任务,在活动结束时这些定时任务的生命周期也结束,这就要求业务服务的定时任务可以自动卸载,我们约定当定时任务生命周期结束时业务服务的定时任务接口应该返回UNINSTALL
错误码,任务调度中心调用定时任务接口时如果接口返回UNINSTALL
错误码则将定时任务从任务调用队列中移除。
根据以上的思路,我们开发了一个足够简单稳定的定时任务调度中心。首先我们针对两种类型的定时任务添加以下两个注解,接口类的注解信息最终会上报的服务注册中心。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public static @interface TimerTask{
int value(); //每隔N秒执行一次(值为整形)
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public static @interface DailyTask{
String value(); //每天特点时间点执行(如:09:00:00)
}
我们希望添加一个名为SendPrize
的定时任务,这个任务每天09:00:00
执行一次,则我们只用给这个类添加DailyTask
注解就可以了,相关代码如下:
@DailyTask("09:00:00")
@WebAppPath("${classname}")
public class SendPrize extends WebApp{
public void process(HttpRequest request, HttpResponse response) throws Exception{
response.setBody("hello world");
}
}