概述
在产品的色彩斑斓的黑的需求中,有存在一类需求,是需要去定时执行的,此时就需要使用到定时任务。例如说,每分钟扫描超时支付的订单,每小时清理一次日志文件,每天统计前一天的数据并生成报表,每个月月初的工资单的推送,每年一次的生日提醒等等。
其中,最喜欢“每个月月初的工资单的推送”,你呢?
在 JDK 中,内置了两个类,可以实现定时任务的功能:
java.util.Timer :可以通过创建 java.util.TimerTask 调度任务,在同一个线程中串行执行,相互影响。也就是说,对于同一个 Timer 里的多个 TimerTask 任务,如果一个 TimerTask 任务在执行中,其它 TimerTask 即使到达执行的时间,也只能排队等待。因为 Timer 是串行的,同时存在 坑坑 ,所以后来 JDK 又推出了 ScheduledExecutorService ,Timer 也基本不再使用。
java.util.concurrent.ScheduledExecutorService :在 JDK 1.5 新增,基于线程池设计的定时任务类,每个调度任务都会被分配到线程池中并发执行,互不影响。这样,ScheduledExecutorService 就解决了 Timer 串行的问题。
在日常开发中,我们很少直接使用 Timer 或 ScheduledExecutorService 来实现定时任务的需求。主要有几点原因:
它们仅支持按照指定频率,不直接支持指定时间的定时调度,需要我们结合 Calendar 自行计算,才能实现复杂时间的调度。例如说,每天、每周五、2019-11-11 等等。
它们是进程级别,而我们为了实现定时任务的高可用,需要部署多个进程。此时需要等多考虑,多个进程下,同一个任务在相同时刻,不能重复执行。
项目可能存在定时任务较多,需要统一的管理,此时不得不进行二次封装。
所以,一般情况下,我们会选择专业的调度任务中间件。
关于“任务”的叫法,也有叫“作业”的。在英文上,有 Task 也有 Job 。本质是一样的,本文两种都会用。
然后,一般来说是调度任务,定时执行。所以胖友会在本文,或者其它文章中,会看到“调度”或“定时”的字眼儿。
在 Spring 体系中,内置了两种定时任务的解决方案:
第一种,Spring Framework 的 Spring Task 模块,提供了轻量级的定时任务的实现。
第二种,Spring Boot 2.0 版本,整合了 Quartz 作业调度框架,提供了功能强大的定时任务的实现。
注:Spring Framework 已经内置了 Quartz 的整合。Spring Boot 1.X 版本未提供 Quartz 的自动化配置,而 2.X 版本提供了支持。
在 Java 生态中,还有非常多优秀的开源的调度任务中间件:
Elastic-Job
唯品会基于 Elastic-Job 之上,演化出了 Saturn 项目。
Apache DolphinScheduler
XXL-JOB
目前国内采用 Elastic-Job 和 XXL-JOB 为主。从了解到的情况,使用 XXL-JOB 的团队会更多一些,主要是上手较为容易,运维功能更为完善。
本文,我们会按照 Spring Task、Quartz、XXL-JOB 的顺序,进行分别入门。而在文章的结尾,会简单聊聊分布式定时任务的实现原理。
快速入门 Spring Task
示例代码对应仓库:lab-28-task-demo 。
考虑到实际场景下,我们很少使用 Spring Task ,所以本小节会写的比较简洁。如果对 Spring Task 比较感兴趣的胖友,可以自己去阅读 《Spring Framework Documentation —— Task Execution and Scheduling》 文档,里面有 Spring Task 相关的详细文档。
在本小节,我们会使用 Spring Task 功能,实现一个每 2 秒打印一行执行日志的定时任务。
引入依赖
在 pom.xml 文件中,引入相关依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lab-28-task-demo</artifactId>
<dependencies>
<!-- 实现对 Spring MVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
因为 Spring Task 是 Spring Framework 的模块,所以在我们引入 spring-boot-starter-web 依赖后,无需特别引入它。
同时,考虑到我们希望让项目启动时,不自动结束 JVM 进程,所以我们引入了 spring-boot-starter-web 依赖。
ScheduleConfiguration
在 cn.iocoder.springboot.lab28.task.config 包路径下,创建 ScheduleConfiguration 类,配置 Spring Task 。代码如下:
// ScheduleConfiguration.java
@Configuration
@EnableScheduling
public class ScheduleConfiguration {
}
在类上,添加 @EnableScheduling 注解,启动 Spring Task 的定时任务调度的功能。
DemoJob
在 cn.iocoder.springboot.lab28.task.job 包路径下,创建 DemoJob 类,示例定时任务类。代码如下:
// DemoJob.java
@Component
public class DemoJob {
private Logger logger = LoggerFactory.getLogger(getClass());
private final AtomicInteger counts = new AtomicInteger();
@Scheduled(fixedRate = 2000)
public void execute() {
logger.info("[execute][定时第 ({}) 次执行]", counts.incrementAndGet());
}
}
在类上,添加 @Component 注解,创建 DemoJob Bean 对象。
创建 #execute() 方法,实现打印日志。同时,在该方法上,添加 @Scheduled 注解,设置每 2 秒执行该方法。
虽然说,@Scheduled 注解,可以添加在一个类上的多个方法上,但是个人习惯上,还是一个 Job 类,一个定时任务。😈
Application
创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
运行 Application 类,启动示例项目。输出日志精简如下:
初始化一个 ThreadPoolTaskScheduler 任务调度器
2019-11-30 18:02:58.415 INFO 83730 --- [main] o.s.s.c.ThreadPoolTaskScheduler: Initializing ExecutorService 'taskScheduler'
每 2 秒,执行一次 DemoJob 的任务
2019-11-30 18:02:58.449 INFO 83730 --- [ pikaqiu-demo-1] c.i.springboot.lab28.task.job.DemoJob : [execute][定时第 (1) 次执行]
2019-11-30 18:03:00.438 INFO 83730 --- [ pikaqiu-demo-1] c.i.springboot.lab28.task.job.DemoJob : [execute][定时第 (2) 次执行]
2019-11-30 18:03:02.442 INFO 83730 --- [ pikaqiu-demo-2] c.i.springboot.lab28.task.job.DemoJob : [execute [定时第 (3) 次执行]
通过日志,我们可以看到,初始化一个 ThreadPoolTaskScheduler 任务调度器。之后,每 2 秒,执行一次 DemoJob 的任务。
至此,我们已经完成了 Spring Task 调度任务功能的入门。实际上,Spring Task 还提供了异步任务 ,这个我们在其它文章中,详细讲解。
下面「2.5 @Scheduled」和「2.6 应用配置文件」两个小节,是补充知识,建议看看。
@Scheduled
@Scheduled 注解,设置定时任务的执行计划。
常用属性如下:
cron 属性:Spring Cron 表达式。例如说,"0 0 12 * * ?" 表示每天中午执行一次,"11 11 11 11 11 ?" 表示 11 月 11 号 11 点 11 分 11 秒执行一次(哈哈哈)。更多示例和讲解,可以看看 《Spring Cron 表达式》 文章。注意,以调用完成时刻为开始计时时间。
fixedDelay 属性:固定执行间隔,单位:毫秒。注意,以调用完成时刻为开始计时时间。
fixedRate 属性:固定执行间隔,单位:毫秒。注意,以调用开始时刻为开始计时时间。
这三个属性,有点雷同,可以看看 《@Scheduled 定时任务的fixedRate、fixedDelay、cron 的区别》 ,一定要分清楚差异。
不常用属性如下:
initialDelay 属性:初始化的定时任务执行延迟,单位:毫秒。
zone 属性:解析 Spring Cron 表达式的所属的时区。默认情况下,使用服务器的本地时区。
initialDelayString 属性:initialDelay 的字符串形式。
fixedDelayString 属性:fixedDelay 的字符串形式。
fixedRateString 属性:fixedRate 的字符串形式。
应用配置文件
在 application.yml 中,添加 Spring Task 定时任务的配置,如下:
spring:
task:
# Spring Task 调度任务的配置,对应 TaskSchedulingProperties 配置类
scheduling:
thread-name-prefix: pikaqiu-demo- # 线程池的线程名的前缀。默认为 scheduling- ,建议根据自己应用来设置
pool:
size: 10 # 线程池大小。默认为 1 ,根据自己应用来设置
shutdown:
await-termination: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
await-termination-period: 60 # 等待任务完成的最大时长,单位为秒。默认为 0 ,根据自己应用来设置
在 spring.task.scheduling 配置项,Spring Task 调度任务的配置,对应 TaskSchedulingProperties 配置类。
Spring Boot TaskSchedulingAutoConfiguration 自动化配置类,实现 Spring Task 的自动配置,创建 ThreadPoolTaskScheduler 基于线程池的任务调度器。本质上,ThreadPoolTaskScheduler 是基于 ScheduledExecutorService 的封装,增强在调度时间上的功能。
注意,spring.task.scheduling.shutdown 配置项,是为了实现 Spring Task 定时任务的优雅关闭。我们想象一下,如果定时任务在执行的过程中,如果应用开始关闭,把定时任务需要使用到的 Spring Bean 进行销毁,例如说数据库连接池,那么此时定时任务还在执行中,一旦需要访问数据库,可能会导致报错。
所以,通过配置 await-termination = true ,实现应用关闭时,等待定时任务执行完成。这样,应用在关闭的时,Spring 会优先等待 ThreadPoolTaskScheduler 执行完任务之后,再开始 Spring Bean 的销毁。
同时,又考虑到我们不可能无限等待定时任务全部执行结束,因此可以配置 await-termination-period = 60 ,等待任务完成的最大时长,单位为秒。具体设置多少的等待时长,可以根据自己应用的需要。