4、Quartz 实战:Spring Boot 集成与动态任务管理
Quartz 这东西,说白了就是 Java 世界里最老牌的任务调度框架。我最早接触它是在 2012 年,那时候还没 Spring Boot,配置全靠 XML,写起来挺痛苦的。现在好了,Spring Boot 把 Quartz 集成得相当丝滑,但很多细节还是得自己注意。
这一章,咱们直接上手。我会带着你从零搭建一个 Spring Boot + Quartz 项目,然后实现任务的动态创建、暂停、恢复,最后聊聊集群部署。嗯,都是我在生产环境里踩过的坑。
本章核心知识点
- Spring Boot 自动配置 Quartz 的原理
- JobDetail、Trigger、Scheduler 三者的关系
- 动态管理任务的 API 使用
- 集群模式下数据库锁的机制
4.1 Spring Boot 集成 Quartz
Spring Boot 从 2.0 开始就内置了对 Quartz 的支持。你只需要引入一个 starter,然后配置一下数据源,基本就能跑了。我个人习惯用 spring-boot-starter-quartz,省心。
先看依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
然后配置 application.yml:
spring:
quartz:
job-store-type: jdbc
jdbc:
initialize-schema: always
properties:
org:
quartz:
scheduler:
instanceName: MyScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
小提示:initialize-schema: always 会在第一次启动时自动创建 Quartz 需要的 11 张表。生产环境建议改成 never,手动执行建表脚本。
写一个简单的 Job 类:
public class SimpleJob implements Job {
@Override
public void execute(JobExecutionContext context) {
String jobName = context.getJobDetail().getKey().getName();
System.out.println("任务 " + jobName + " 执行时间: " + LocalDateTime.now());
}
}
再配一个 Trigger:
@Configuration
public class QuartzConfig {
@Bean
public JobDetail simpleJobDetail() {
return JobBuilder.newJob(SimpleJob.class)
.withIdentity("simpleJob", "group1")
.storeDurably()
.build();
}
@Bean
public Trigger simpleTrigger() {
return TriggerBuilder.newTrigger()
.forJob(simpleJobDetail())
.withIdentity("simpleTrigger", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))
.build();
}
}
启动项目,每 5 秒就会打印一次日志。嗯,就这么简单。但这里有个坑——Job 类必须有无参构造器,Quartz 是通过反射创建实例的。我曾经见过有人给 Job 加了 @Autowired 构造器注入,结果启动就报错。
4.2 动态创建、暂停、恢复任务
静态配置只适合固定任务。真实项目里,任务往往是用户在前端页面创建的。比如运营后台要配置一个定时推送通知,总不能每次改代码重启吧?
这时候就需要动态管理了。核心是注入 Scheduler 对象,然后调用它的 API。
4.2.1 动态创建任务
@Service
public class DynamicJobService {
@Autowired
private Scheduler scheduler;
public void createJob(String jobName, String group, String cron) throws SchedulerException {
JobDetail jobDetail = JobBuilder.newJob(DynamicJob.class)
.withIdentity(jobName, group)
.storeDurably()
.build();
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobName + "Trigger", group)
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
scheduler.scheduleJob(jobDetail, trigger);
}
}
这里要注意,DynamicJob 是一个通用的 Job 实现类,它从 JobDataMap 里读取具体业务参数:
public class DynamicJob implements Job {
@Override
public void execute(JobExecutionContext context) {
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String businessKey = dataMap.getString("businessKey");
// 根据 businessKey 执行具体业务逻辑
System.out.println("执行业务: " + businessKey);
}
}
4.2.2 暂停与恢复
暂停任务:
public void pauseJob(String jobName, String group) throws SchedulerException {
JobKey jobKey = new JobKey(jobName, group);
scheduler.pauseJob(jobKey);
}
恢复任务:
public void resumeJob(String jobName, String group) throws SchedulerException {
JobKey jobKey = new JobKey(jobName, group);
scheduler.resumeJob(jobKey);
}
删除任务:
public void deleteJob(String jobName, String group) throws SchedulerException {
JobKey jobKey = new JobKey(jobName, group);
scheduler.deleteJob(jobKey);
}
注意:删除任务前,最好先暂停它。否则如果任务正在执行,删除操作可能会抛异常。我曾经在生产环境遇到过这个问题,当时任务执行时间较长,删除时直接报了 JobPersistenceException。
4.3 集群部署方案
单机 Quartz 有个致命问题——如果服务器挂了,所有任务都停了。生产环境必须上集群。
Quartz 的集群方案其实不复杂,核心思想是:多个节点共享同一个数据库,通过数据库行级锁来竞争任务执行权。
配置集群模式,只需要改两个地方:
spring:
quartz:
properties:
org:
quartz:
jobStore:
isClustered: true
clusterCheckinInterval: 20000
scheduler:
instanceId: AUTO
解释一下这几个参数:
| 参数 | 说明 | 建议值 |
|---|---|---|
| isClustered | 开启集群模式 | true |
| clusterCheckinInterval | 节点心跳检测间隔(毫秒) | 20000 |
| instanceId | 节点唯一标识,AUTO 自动生成 | AUTO |
集群模式下,Quartz 会使用 QRTZ_LOCKS 表来加锁。当一个节点要执行任务时,它会先尝试获取锁。获取成功的节点执行,其他节点等待。
集群部署的关键点
- 所有节点必须使用同一个数据库
- 各节点的系统时间必须同步(建议用 NTP)
- 每个节点的 instanceId 必须唯一
- 任务默认不会重复执行,因为锁机制保证了互斥
你可能会问:如果一个节点挂了,任务会不会丢?不会。Quartz 的集群容错机制是这样的:每个节点会定期向数据库写入心跳。如果某个节点超过 clusterCheckinInterval 的 2 倍时间没有更新心跳,其他节点就会认为它挂了,然后接管它的任务。
我曾经遇到过一个坑:时间不同步导致任务被重复执行。当时有两台服务器,一台快了 30 秒,结果两台都认为自己该执行任务,锁机制没拦住。从那以后,我每次部署集群都会检查 NTP 配置。
4.4 实战中的避坑指南
最后,分享几个我踩过的坑:
- Job 类不要用 @Scope("prototype")。Quartz 自己管理 Job 实例的生命周期,Spring 的 scope 不生效。
- JobDataMap 只支持基本类型和 String。如果你存了一个复杂对象,反序列化时会报错。我一般只存业务主键,具体数据从数据库查。
- 集群模式下,Cron 表达式不要写太密集。比如每秒执行一次,锁竞争会很激烈,反而降低性能。
- 数据库表要定期清理。Quartz 的
QRTZ_TRIGGERS和QRTZ_FIRED_TRIGGERS表会积累大量历史数据,建议写个定时任务清理 7 天前的记录。
我的建议:如果项目刚起步,任务量不大,用单机模式就够了。等业务量上来,再切换到集群模式。Quartz 的集群配置是运行时生效的,不需要改代码,只需要改配置文件和数据库连接。