美文网首页互联网科技程序员
我重构定时任务服务时,运用的那些编程思想

我重构定时任务服务时,运用的那些编程思想

作者: 风平浪静如码 | 来源:发表于2019-11-01 15:34 被阅读0次

    在重构一个老项目的一个定时任务服务的过程中,我想到了几个有趣的点子,整个服务的骨架就是借鉴这几个点子搭建的。

    定时任务服务改造的几个阶段

    一开始想做的,只是能让定时任务实现可页面配置,可随时修改配置随时生效。配置指的是配置cron表达式,定义任务的执行时机。但由于后期的种种问题,不得不对定时任务服务进行再次改造,所以,定时任务服务经历了三个阶段。

    第一个阶段:
    目的:定时任务做成可配置。
    缺点:发现定时任务都很耗内存,且由于执行时间过长,通常几分钟的都有,这样就会有任务碰撞到一起执行的情况,至少CPU长期百分百使用状态。比如报表统计类任务,大多定义在每个小时的前10分钟内完成。

    第二个阶段:
    目的:减少内存和降低CPU的使用率。
    方案:将定时任务串行化执行,由一个单一线程的线程池去执行。
    缺点:将任务串行化执行后,会有风险。比如因某个卡住了,导致后面的任务都得不到执行。

    第三阶段:
    目的:解决串行化执行的弊端。
    方案:引入监视器。如果有任务从提交到执行,时间超过15分钟还未完成,就直接中断线程,让下个任务能够得以执行,并发送邮件通知便于排查原因。

    每个阶段借鉴的思想

    1、引导器

    之前学汇编的时候知道操作系统有个引导器的存在,就是在系统盘的某个开始位置,由主板上的程序加载执行,系统再由引导器启动。定时任务也应该有一个启动器来初始化配置并提交到调度线程池,所以我借鉴了系统引导器的设计。

    要求所有定时任务都实现定时任务接口TimeTaskPlayer。因为调度线程池要求submit是一个Runnable,所以定时任务接口要继承Runnable接口,由 run 方法调用子类实现的startPlayer方法。至于为什么不让子类(定时任务)直接实现run方法,后面会有用处。

    定义引导器接口

    实现定时任务启动引导器

    在Spring boot初始完成后,调用引导器初始化服务

    当然,优雅退出肯定也不能少呀,其实可以直接使用spring的优雅退出的,都是使用的同一个原理,注册jvm钩子。

    提供一个所有任务的ScheduleFuture的持有者,提供停止所有任务的方法,用于更新配置后取消所有定时任务,由引导器重新启动。即更新配置后重启所有定时任务。

    任务的Cron表达式配置管理类,提供reloadCronFromDB方法给接口调用更新任务的cron表达式缓存。这里的注释有改动,存的不是完整类名,而且去掉包名后的类名,同时Bean的name(spring管理)也是去掉包名后的类名,首字母大写。

    三个方法很好理解,一个是根据定时任务的Class获取cron表达式,如果缓存没有,则从数据库加载。第二个是获取定时任务的状态,用于控制是否启用这个定时任务。

    当然,还有使用Aop添加任务执行异常邮件通知,这里就不贴了。

    2、插件

    如何将定时任务控制串行执行,且不改动现有代码呢,如果改动太大就相当于重构了。这时候我想到了插件。插件我们常常用到,比如idea就有很多插件,再与我们贴近点的就是Mybatis的分页插件。插件,无外呼就是在某些任务开始之前插入埋点代码,其实也是AOP编程思想。所以我借鉴了插件这一思想,来实现不修改现有代码的情况下将定时任务串行执行。这里使用了观察者模式。

    观察者模式:抽象观察者

    观察者模式:抽象主题

    观察者模式:具体的定时任务事件执行者,即观察者。这里包含了监听器的内容,就是将事件转为任务放入单线程的线程池后,拿到Future,交给监听器监控任务的执行状态。

    观察者模式:具体的事件主题,接收事件并通知对该事件感兴趣的观察者。

    那么,何时发布的事件呢?就是定时任务到执行时间的时候。文章开头就埋下了一个点,就是定时任务接口TimedTaskPlayer为何不让子类直接实现run方法,为的就是可以在不改任务代码的情况下,实现让定时任务改为串行执行。

    修改后的TimedTaskPlayer接口如下图,注意看run方法,神不知鬼不觉的就能将任务的执行权转交出去。定时任务就只是一个任务的执行时间节点的掌控者,不再是任务执行的掌控者,简简单单的就被抽空了身体。

    3、监视器

    如何杜绝串行任务因单个任务阻塞导致服务崩溃呢?当我们使用idea编码的时候,因打开的软件太多,就会导致系统变卡,但是我们可以通过系统进程监视器看到idea卡住了,我们可以选择手动杀掉重启。

    所以,我想我的定时任务系统也能有这样的功能。加入监视器,在任务提交到单线程线程池时,也将返回的Future提交到监视队列,由监视器线程轮询队列中任务的执行情况,发现超时未执行完的任务直接中断执行,否则将任务放入监视队列末尾。这里的超时目前我只能拿任务的提交时间和当前时间计算。

    变种的设计模式之策略模式

    定时任务模块中还有一个消息订阅消费的小模块,当然这与定时任务没有关系。这里我用到了一种设置模式,叫条件执行器。啥?正如过滤器与拦截器是责任链的一种变种一样,条件执行器也是策略模式的一种变种,当然条件执行器是我乱叫的。

    为啥叫条件执行器,在使用switch分支语句的时候,我们可以定义case1、2、3执行某个逻辑,case4执行某个逻辑。一样的,一条消息可能会有很多条件执行器感兴趣,也可能没有任何条件执行器感兴趣,也可能只有一个条件执行器感兴趣。与switch很像,所以我叫它条件执行器。当然,这类消息属于通知类消息,无论消费成功或失败,都不会再有第二次消费。

    总结

    定时任务串行化执行有风险,但却是为了能在4g内存的机器上跑起来。但是,如果出现有任务把线程堵住的情况,那就是代码有问题,如果是代码的问题,即便是多线程,风险一样存在,甚至更高。为何这个说,假如一个任务3分钟执行一次,结果每次都把线程堵住,要么把内存玩爆,要么把线程池队列阻塞满,最后还不是一样的下场。

    当然,并非所有业务场景都适用,如果对定时任务要求及时的,就不能这么用,比如我一定要让这个任务0点0分执行。或者当任务越来越多的时候,比如有上百个,上百个任务串行执行想下什么后果。

    相关文章

      网友评论

        本文标题:我重构定时任务服务时,运用的那些编程思想

        本文链接:https://www.haomeiwen.com/subject/vwnfbctx.html