定时任务分类
定时任务分为分布式定时任务和单机定时任务两个大的方向,他们的适用场景不同。
单机定时任务在单台计算机上运行,其执行结果和单台机器上的数据有关,如对本地机器的缓存做核对、清理日志等。它的 优点 是简单易用,无需额外的配置和部署;缺点是任务一般没有持久化机制,重启后有任务丢失的问题。
分布式定时任务,将任务交由集群中的某个机器来执行,常用于需要在分布式环境下协同执行的任务,例如处理耗时较长的任务、数据同步、消息处理、超时关单等场景。它的优点是高可靠性、高可扩展性和分布式特性;缺点视具体的组件而定。
应用场景
- 超时关单场景:每笔订单创建后,10min未推进支付状态,即执行关单操作
- 生成订单时,提交一个10min后的分布式关单任务,完成关闭内部订单和其他相关通知操作。可对该任务指定不同的执行间隔时间。
- 使用MQ,将消息投递到延迟消息队列中,指定时间间隔后,再将消息投递给消费者。
- 资金回退(FundBack)场景:支付回调接口中,内部订单已经由钱包B通知支付成功了,但此时收到A钱包的通知支付成功消息,此时需要告知A支付失败且通知A进行 FundBack 操作。
- 生成FundBack任务,指定不同间隔时间来通知A,避免因钱包A发布期间无法收到FundBack信息的问题。
- 核对本地缓存数据:分布式多级缓存中,需要扫描缓存和DB中的数据是否一致。
- 建设本地定时任务完成即可。
本地定时任务框架
单机定时任务有 Timer、ScheduledExecutorService、SpringTask、SpringQuartz 等。Timer、ScheduledExecutorService 都无法使用 Cron 表达式指定任务执行的具体时间,灵活性不够,本文不做展开。
SpringTask
org.springframework.scheduling.annotation.Scheduled提供的属性如下:
/**
* A cron-like expression, extending the usual UN*X definition to include triggers
* on the second, minute, hour, day of month, month, and day of week.
* <p>For example, {@code "0 * * * * MON-FRI"} means once per minute on weekdays
* (at the top of the minute - the 0th second).
* <p>The fields read from left to right are interpreted as follows.
* <ul>
* <li>second</li>
* <li>minute</li>
* <li>hour</li>
* <li>day of month</li>
* <li>month</li>
* <li>day of week</li>
* </ul>
* <p>The special value {@link #CRON_DISABLED "-"} indicates a disabled cron
* trigger, primarily meant for externally specified values resolved by a
* <code>${...}</code> placeholder.
* @return an expression that can be parsed to a cron schedule
* @see org.springframework.scheduling.support.CronSequenceGenerator
*/
String cron() default "";
/**
* A time zone for which the cron expression will be resolved. By default, this
* attribute is the empty String (i.e. the server's local time zone will be used).
* @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)},
* or an empty String to indicate the server's default time zone
* @since 4.0
* @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone)
* @see java.util.TimeZone
*/
String zone() default "";
/**
* Execute the annotated method with a fixed period in milliseconds between the
* end of the last invocation and the start of the next.
* @return the delay in milliseconds
*/
long fixedDelay() default -1;
/**
* Execute the annotated method with a fixed period in milliseconds between the
* end of the last invocation and the start of the next.
* @return the delay in milliseconds as a String value, e.g. a placeholder
* or a {@link java.time.Duration#parse java.time.Duration} compliant value
* @since 3.2.2
*/
String fixedDelayString() default "";
/**
* Execute the annotated method with a fixed period in milliseconds between
* invocations.
* @return the period in milliseconds
*/
long fixedRate() default -1;
/**
* Execute the annotated method with a fixed period in milliseconds between
* invocations.
* @return the period in milliseconds as a String value, e.g. a placeholder
* or a {@link java.time.Duration#parse java.time.Duration} compliant value
* @since 3.2.2
*/
String fixedRateString() default "";
/**
* Number of milliseconds to delay before the first execution of a
* {@link #fixedRate} or {@link #fixedDelay} task.
* @return the initial delay in milliseconds
* @since 3.2
*/
long initialDelay() default -1;
/**
* Number of milliseconds to delay before the first execution of a
* {@link #fixedRate} or {@link #fixedDelay} task.
* @return the initial delay in milliseconds as a String value, e.g. a placeholder
* or a {@link java.time.Duration#parse java.time.Duration} compliant value
* @since 3.2.2
*/
String initialDelayString() default "";
执行原理:
ScheduledTaskRegistrar#afterPropertiesSet 方法中,默认初始化一个单线程的 ScheduledExecutorService 来执行任务。
SpringTask 在 ScheduledAnnotationBeanPostProcessor#processScheduled 中解析和收集 Scheduled 注解中的参数;然后向 ScheduledTaskRegistrar 中添加对应类型的任务。
ScheduledExecutorService:
总结起来就是通过线程池来执行任务;DelayedWorkQueue 作为阻塞队列,并排序任务定时执行;ScheduledFutureTask 记录任务定时信息,。
ScheduledExecutorService 继承线程池,也是把任务提交给线程池执行,只不过它的任务类进行了扩展。
ScheduledExecutorService 自定义了阻塞队列 DelayedWorkQueue 给线程池使用,它可以根据 ScheduledFutureTask 的下次执行时间来阻塞 take 方法,并且新进来的 ScheduledFutureTask 会根据这个时间来进行排序,最小的最前面。
任务类 ScheduledFutureTask 继承 FutureTask 并扩展了一些属性来记录任务下次执行时间和每次执行间隔。同时重写了run方法重新计算任务下次执行时间,并把任务放到线程池队列中。
spring task 的优缺点:
优点:
- spring框架自带的定时功能,开启和定义定时任务非常容易,支持复杂的 cron 表达式,可以满足绝大多数单机版的业务场景。单线程执行任务时,当前次的调度完成后,再执行下一次任务调度。
缺点:
- 默认单线程,如果前面的任务执行时间太长,对后面任务的执行有影响。
- 不支持集群方式部署,提交的任务在单机内存中,可能出现任务丢失的情况
SpringQuartz
优点:
- 默认是多线程异步执行,单个任务时,在上一个调度未完成时,下一个调度时间到时,会另起一个线程开始新的调度,多个任务之间互不影响。
- 支持复杂的 cron 表达式
- 它能被集群实例化,支持分布式部署。
缺点:
- 相对于spring task实现定时任务成本更高,需要手动配置 QuartzJobBean 、 JobDetail和 Trigger 等。需要引入了第三方的 quartz 包,有一定的学习成本。
- 没有内置 UI 管理控制台。
- 不支持并行调度,不支持失败处理策略和动态分片的策略等。
代码案例
分布式定时任务框架
XXL-Job
将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。
将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。
调度中心:
自研调度组件(早期调度组件基于Quartz);一方面是为了精简系统降低冗余依赖,另一方面是为了提供系统的可控度与稳定性;
执行器:
执行器实际上是一个内嵌的Server,在项目启动时,执行器会通过 “@JobHandler” 识别Spring容器中“Bean模式任务”,以注解的value属性为key管理起来。
“执行器”接收到“调度中心”的调度请求时,如果任务类型为“Bean模式”,将会匹配Spring容器中的“Bean模式任务”,然后调用其execute方法,执行任务逻辑。如果任务类型为“GLUE模式”,将会加载GLue代码,实例化Java对象,注入依赖的Spring服务(注意:Glue代码中注入的Spring服务,必须存在与该“执行器”项目的Spring容器中),然后调用execute方法,执行任务逻辑。
任务注册与发现
任务注册以 “执行器” 为最小粒度进行注册。
基于 HTTP 协议的 REST接口(未使用ZK),将执行器注册到 调度中心 DB 的注册表中,再通过对应的 Beat 完成任务注册、执行器注册、动态任务发现、注册信息失效等动作。
优缺点:
优点:
- 有界面管理定时任务,支持弹性扩容缩容、动态分片、故障转移、失败报警等功能。
- 支持在运行时动态创建任务( XxlJobHelper#submitCronTask),参见:xxl-job issue277
缺点:
- 一致性:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行。和 quartz 一样,通过数据库分布式锁,来控制任务不能重复执行
- 调度中心HA(中心式):调度采用中心式设计,“调度中心”自研调度组件并支持集群部署,可保证调度中心HA。不同于 Elastic-Job 的去中心化设计, XXL-JOB 的这种设计也被称为中心化设计
官网-架构