在重构一个老项目的一个定时任务服务的过程当中,我想到了几个有趣的点子,整个服务的骨架就是借鉴这几个点子搭建的。spring
一开始想作的,只是能让定时任务实现可页面配置,可随时修改配置随时生效。配置指的是配置cron表达式,定义任务的执行时机。但因为后期的种种问题,不得不对定时任务服务进行再次改造,因此,定时任务服务经历了三个阶段。数据库
第一个阶段:
目的:定时任务作成可配置。
缺点:发现定时任务都很耗内存,且因为执行时间过长,一般几分钟的都有,这样就会有任务碰撞到一块儿执行的状况,至少CPU长期百分百使用状态。好比报表统计类任务,大多定义在每一个小时的前10分钟内完成。编程
第二个阶段:
目的:减小内存和下降CPU的使用率。
方案:将定时任务串行化执行,由一个单一线程的线程池去执行。
缺点:将任务串行化执行后,会有风险。好比因某个卡住了,致使后面的任务都得不到执行。设计模式
第三阶段:
目的:解决串行化执行的弊端。
方案:引入监视器。若是有任务从提交到执行,时间超过15分钟还未完成,就直接中断线程,让下个任务可以得以执行,并发送邮件通知便于排查缘由。缓存
以前学汇编的时候知道操做系统有个引导器的存在,就是在系统盘的某个开始位置,由主板上的程序加载执行,系统再由引导器启动。定时任务也应该有一个启动器来初始化配置并提交到调度线程池,因此我借鉴了系统引导器的设计。多线程
要求全部定时任务都实现定时任务接口TimeTaskPlayer。由于调度线程池要求submit是一个Runnable,因此定时任务接口要继承Runnable接口,由 run 方法调用子类实现的startPlayer方法。至于为何不让子类(定时任务)直接实现run方法,后面会有用处。并发
定义引导器接口jvm
实现定时任务启动引导器ide
在Spring boot初始完成后,调用引导器初始化服务编码
固然,优雅退出确定也不能少呀,其实能够直接使用spring的优雅退出的,都是使用的同一个原理,注册jvm钩子。
提供一个全部任务的ScheduleFuture的持有者,提供中止全部任务的方法,用于更新配置后取消全部定时任务,由引导器从新启动。即更新配置后重启全部定时任务。
任务的Cron表达式配置管理类,提供reloadCronFromDB方法给接口调用更新任务的cron表达式缓存。这里的注释有改动,存的不是完整类名,并且去掉包名后的类名,同时Bean的name(spring管理)也是去掉包名后的类名,首字母大写。
三个方法很好理解,一个是根据定时任务的Class获取cron表达式,若是缓存没有,则从数据库加载。第二个是获取定时任务的状态,用于控制是否启用这个定时任务。
固然,还有使用Aop添加任务执行异常邮件通知,这里就不贴了。
如何将定时任务控制串行执行,且不改动现有代码呢,若是改动太大就至关于重构了。这时候我想到了插件。插件咱们经常用到,好比idea就有不少插件,再与咱们贴近点的就是Mybatis的分页插件。插件,无外呼就是在某些任务开始以前插入埋点代码,其实也是AOP编程思想。因此我借鉴了插件这一思想,来实现不修改现有代码的状况下将定时任务串行执行。这里使用了观察者模式。
观察者模式:抽象观察者
观察者模式:抽象主题
观察者模式:具体的定时任务事件执行者,即观察者。这里包含了监听器的内容,就是将事件转为任务放入单线程的线程池后,拿到Future,交给监听器监控任务的执行状态。
观察者模式:具体的事件主题,接收事件并通知对该事件感兴趣的观察者。
那么,什么时候发布的事件呢?就是定时任务到执行时间的时候。文章开头就埋下了一个点,就是定时任务接口TimedTaskPlayer为什么不让子类直接实现run方法,为的就是能够在不改任务代码的状况下,实现让定时任务改成串行执行。
修改后的TimedTaskPlayer接口以下图,注意看run方法,神不知鬼不觉的就能将任务的执行权转交出去。定时任务就只是一个任务的执行时间节点的掌控者,再也不是任务执行的掌控者,简简单单的就被抽空了身体。
如何杜绝串行任务因单个任务阻塞致使服务崩溃呢?当咱们使用idea编码的时候,因打开的软件太多,就会致使系统变卡,可是咱们能够经过系统进程监视器看到idea卡住了,咱们能够选择手动杀掉重启。
因此,我想个人定时任务系统也能有这样的功能。加入监视器,在任务提交到单线程线程池时,也将返回的Future提交到监视队列,由监视器线程轮询队列中任务的执行状况,发现超时未执行完的任务直接中断执行,不然将任务放入监视队列末尾。这里的超时目前我只能拿任务的提交时间和当前时间计算。
定时任务模块中还有一个消息订阅消费的小模块,固然这与定时任务没有关系。这里我用到了一种设置模式,叫条件执行器。啥?正如过滤器与拦截器是责任链的一种变种同样,条件执行器也是策略模式的一种变种,固然条件执行器是我乱叫的。
为啥叫条件执行器,在使用switch分支语句的时候,咱们能够定义case一、二、3执行某个逻辑,case4执行某个逻辑。同样的,一条消息可能会有不少条件执行器感兴趣,也可能没有任何条件执行器感兴趣,也可能只有一个条件执行器感兴趣。与switch很像,因此我叫它条件执行器。固然,这类消息属于通知类消息,不管消费成功或失败,都不会再有第二次消费。
定时任务串行化执行有风险,但倒是为了能在4g内存的机器上跑起来。可是,若是出现有任务把线程堵住的状况,那就是代码有问题,若是是代码的问题,即使是多线程,风险同样存在,甚至更高。为什么这个说,假如一个任务3分钟执行一次,结果每次都把线程堵住,要么把内存玩爆,要么把线程池队列阻塞满,最后还不是同样的下场。
固然,并不是全部业务场景都适用,若是对定时任务要求及时的,就不能这么用,好比我必定要让这个任务0点0分执行。或者当任务愈来愈多的时候,好比有上百个,上百个任务串行执行想下什么后果。