设计一个高效的定时任务系统( 四 )


既然谈到了设计,是不是要先出一版产品需求文档呢 。这个真的可以有,我们先提提需求再聊聊方案 。
你要的需求设计
定时任务系统的核心功能是什么?既然是第一版,我们不要那些花里胡哨,锦上添花的功能,从本质出发 。

设计一个高效的定时任务系统

文章插图
 
我理解应该有三个核心模块:
任务录入:提供录入定时任务的入口,支持最基本的定时任务机制:cron 表达式,自定义执行时间等等方式 。
任务调度:通过合适的调度算法从任务库中触发到期的任务以期执行,当然调度系统最好不要直接参数执行,做好自己的事即可 。
任务执行:调度系统已经触发了任务,那么可以由专门的执行系统来负责任务执行,执行不会阻塞任务调度,纵然执行有阻塞也是在执行系统中阻塞,保持调度的可用性 。
以上 3 个模块就能满足基本的任务系统需求,接下来聊聊实现方案 。
技术实现方案
①录入模块实现
一般执行定时任务的场景是:每隔多久执行一次操作,这种在业务系统中最常见的就是使用 cron 表达式来代替,所以录入模块要做到可以解析 cron 表达式即可 。
这种录入模式主要是针对后台手动录入任务的场景,对于开发人员来说最优解就是能用代码实现就不去切换鼠标(有同学说能点点鼠标谁还去砌砖) 。
所以还需要提供可执行 jar 包用于业务系统集成,方便开发人员通过编码的方式将任务录入到系统 。
总结一下录入任务的两种途径:
  • 提供业务系统可集成 jar 包,由开发人员编码录入任务 。
  • 提供管理后台界面,提供可配置方式录入任务 。
对于业务代码植入式的任务业务服务器启动的时候会通过 jar 包把任务推送过来,对于后台录入的任务那就需要入库保存 。
②调度模块实现
在拿到录入模块的定时任务配置信息之后接下来要做的事情:将 cron 表达式变为一个个可执行的时间点 。
比如在 Spring 中就已经提供解析 cron 的功能:CronSequenceGenerator 类可以帮我们执行此操作 。
有了可执行时间点之后要做的事情就是管理它,让它调度起来 。上面我们讨论过的各种调度算法此时可以派上用场 。
如果任务密度不是很大,多为固定的定期执行任务,小根堆算法就可以胜任;如果任务密集,很多短期快速执行的任务,可以采用时间轮的方式提高效率 。
另外,比如有个任务是 5 分钟执行一次,那么你一次要解析出来多少个可执行的时间点?一天,一周,一个月?
这样肯定是有问题的,目前的实现方案是任务首次启动的时候给出第一次执行的时间,每次执行的时候去计算下次任务开始的时间 。
【设计一个高效的定时任务系统】这里有一个点:JAVA 相关的框架现在实现的方案都是当前任务执行完成之后再计算下次任务开始执行的时间 。
如果任务是 5 分钟一次,当前时间是:10:00,第一个任务完成需要 6 分钟,那么第二个任务开始的时间就是:
设计一个高效的定时任务系统

文章插图
 
我们预期是每隔 5 分钟执行一次,事实上除了第一次是按照预期的准点执行以外,后面都会在绝对时间上有延期 。
到这里我们解决了两个问题:
  • 解析时间表达式为时间点,如何确认周期性任务的下一个可执行时间点 。
  • 将可执行时间点送入调度器中,让时间流动起来 。
③任务执行模块
任务录入,任务调度我们都完成了,执行模块才是最后的重头戏 。这里我们再细化一下,任务录入不能说只是把任务所属的表达式载入系统就完事,要把任务对象化,达到招手即用的状态 。
这里我们把每个任务都封装为一个对象 Job,所有的 Job 都在内存中加载,调度器定义为 Scheduler,把每个可执行时间封装为 Trigger 对象 。
Trigger 用于定义调度任务的事件规则,唯一关联一个 Job 并标识当前 Job 的执行状态 。
上图就是我们的极简版定时任务系统核心功能,怎么样,麻雀虽小,五脏俱全 。该有的功能一样不少,不该有的功能一个都没有 。
设计一个高效的定时任务系统

文章插图
 
到这里为止我们已经输出了极简版定时任务调度系统的核心设计和实现方案,依据这个方案你可以实现定时任务调度系统的单机版核心功能 。
我们先不提加需求的问题,先来个高可用的问题,上面的方案是将任务加载到一台机器的内存中定时执行,那么如果要实现高可用,多台机器的情况任务如何防止多次执行呢?


推荐阅读