定时任务:历史 & 应用
在现实生活中,有许多需要定时执行的操作。比如:每日/每月计算利息、每月的财务报表、月底的工资发放等等。在软件中,我们一般通过定时任务来实现。
同时,定时任务和消息队列一样,也可以达到异步化的效果。对于大量的任务,通过定时触发,按计算资源调度的方式,可以避免机器被打爆。
crontab & Quartz
在操作系统层面,定时任务有两种实现方式:轮询和中断。我们可以采用 while 循环,不停判断时钟是否到确定的时刻,来判断是否拉起任务执行。也可以对固定时刻设置一个中断,到时触发。显然的,对于实时性要求不那么苛刻的场景,一定是中断的方式会比较有效。
在 linux 操作系统层中,有一个 crontab 命令支持定时的任务调度。
在 java 中,jdk 自身就带 Timer,可以满足定期执行的效果。但是,定时任务除了需要定期触发外,还需要对任务有持久化的能力。所以,在单体系统中,最流行的是实用 Quartz 框架。
Quartz 通常有三部分组成:调度器(Scheduler)、任务(JobDetail)、触发器(Trigger,包括 SimpleTrigger 和 CronTrigger)。
定义执行任务的 Job,这里要实现 Quartz 提供的 Job 接口:
创建 Scheduler 和 Trigger,并执行定时任务:
分布式
在分布式系统中。我们需要定时执行的多个任务可以分配到集群的不同机器中执行,充分利用资源。所以在分布式系统中的定时任务,我们可以理解为:定时触发+集群调度。
最简单的,我们可以采用 Quartz+YARN 的方式来实现。
但是 Quartz 如果在单机,也可以有稳定性风险,所以可以采用集群 Quartz+选主机制,来保证定时触发的高可用。
定时任务的开发规范
不讨论定时触发,聚焦在定时触发后任务的逻辑上。
首先,定时任务的触发由于延迟、多活等因素,我们不能保证定时任务只被触发一次。所以,我们需要保证触发多次的情况下的不重入。
这里有两种做法:
悲观锁方式:不论是用数据库锁还是分布式锁,在执行任务前先锁定任务,然后判断任务状态。状态为已执行就忽略本次调度。
乐观锁方式:这种方式下,需要任务的执行基本满足函数式的要求,在执行的过程中不进行写入操作。执行完后,进行幂等的写入。
其次,定时任务的执行可以分为单条数据的处理和数据汇总处理两类。
对于单条数据处理,无论悲观锁和乐观锁的方式,效果都类似。
对于数据汇总处理的,建议采用悲锁的方式。同时,数据的处理如果量比较大,考虑引入 map-reduce。
最后,定时任务触发,需要考虑部分时点触发失效的场景:
任务的捞取逻辑尽量不要依赖调度周期,可以通过状态而不是时间区间。
不要用定时任务实现比较依赖实效性的工作。
版权声明: 本文为 InfoQ 作者【agnostic】的原创文章。
原文链接:【http://xie.infoq.cn/article/6d2d3ba1d919ce8a097f7a971】。文章转载请联系作者。
评论