3、Quartz 深度解析:核心概念与实战配置
Quartz 这个框架,说实话,在 Java 调度领域算是老前辈了。我从 1.6 版本就开始用,到现在 3.x 版本,核心思想一直没变。今天咱们就把它的三个核心概念掰开揉碎,再聊聊 Cron 表达式和持久化那些坑。
3.1 三大核心概念:Job、Trigger、Scheduler
Quartz 的设计其实很直观。你想想看,一个调度系统无非就是三件事:做什么、什么时候做、谁来管。对应到 Quartz 里,就是 Job、Trigger 和 Scheduler。
一句话总结:Job 定义任务逻辑,Trigger 定义触发规则,Scheduler 把两者绑在一起运行。
3.1.1 Job — 任务本体
Job 就是一个接口,你实现它的 execute(JobExecutionContext context) 方法就行。我个人习惯把业务逻辑写在 Service 层,Job 里只做调用和异常处理。
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String taskName = dataMap.getString("taskName");
System.out.println("执行任务: " + taskName + ",时间: " + new Date());
}
}
这里有个细节——Job 是无状态的。每次触发都会新建一个 Job 实例。如果你想传参数,得用 JobDataMap。我在项目中遇到过有人把数据库连接放在 Job 的成员变量里,结果并发执行时数据全乱了。嗯,千万别这么干。
3.1.2 Trigger — 触发规则
Trigger 决定任务什么时候跑。Quartz 提供了两种主流触发器:
| 触发器类型 | 适用场景 | 特点 |
|---|---|---|
| SimpleTrigger | 固定间隔执行(如每 5 分钟一次) | 支持重复次数、间隔毫秒数 |
| CronTrigger | 复杂时间规则(如每周一 10:00) | 基于 Cron 表达式,灵活但易出错 |
实际项目中,SimpleTrigger 用得少。因为大部分业务需求都是「每天凌晨 2 点跑」「每月 1 号生成报表」这类复杂规则。所以 CronTrigger 才是主力。
3.1.3 Scheduler — 调度器
Scheduler 是 Quartz 的大脑。它负责管理 Job 和 Trigger 的注册、启动、暂停、停止。你可以把它理解成一个容器,里面装着很多 JobDetail 和 Trigger 的配对。
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("myJob", "group1")
.usingJobData("taskName", "数据同步任务")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?"))
.build();
scheduler.scheduleJob(job, trigger);
scheduler.start();
这段代码我写过无数次了。注意 withIdentity 里的 group 参数——它用来做任务分组。我曾经在一个项目里没分组,结果不同模块的任务名冲突了,调试了一下午才发现。
我的习惯:按业务模块分 group,比如 "orderGroup"、"reportGroup"、"cleanGroup"。这样管理起来清晰很多。
3.2 Cron 表达式详解
Cron 表达式是 Quartz 的核心技能,也是翻车重灾区。说白了,它就是 7 个字段组成的字符串,用来描述时间规则。
3.2.1 格式与字段
| 位置 | 字段 | 取值范围 | 允许的特殊字符 |
|---|---|---|---|
| 1 | 秒 | 0-59 | , - * / |
| 2 | 分 | 0-59 | , - * / |
| 3 | 时 | 0-23 | , - * / |
| 4 | 日 | 1-31 | , - * ? / L W |
| 5 | 月 | 1-12 或 JAN-DEC | , - * / |
| 6 | 周 | 1-7 或 SUN-SAT | , - * ? / L # |
| 7 | 年(可选) | 1970-2099 | , - * / |
这里有个容易混淆的点:日和周不能同时指定。如果你在日字段写了具体数字,周字段就必须用 ?,反过来也一样。? 表示「不指定」,这是 Quartz 特有的。
3.2.2 常用表达式示例
0 0 2 * * ?— 每天凌晨 2 点执行0 0/5 * * * ?— 每 5 分钟执行一次0 0 9-18 * * MON-FRI— 工作日 9 点到 18 点每小时执行0 0 0 1 * ?— 每月 1 号零点执行0 15 10 ? * 6L— 每月最后一个周五 10:15 执行
我曾经踩过的坑:写 Cron 表达式时把日和周都填了具体值,结果任务要么不触发,要么乱触发。记住:日和周用 ? 互斥,这是 Quartz 的硬规则。
3.2.3 特殊字符说明
*— 所有值。比如*在分字段表示每分钟?— 不指定。只用于日和周字段-— 范围。比如9-18表示 9 到 18 点/— 步长。比如0/5表示从 0 开始每 5 个单位L— 最后。比如L在日字段表示当月最后一天W— 最近工作日。比如15W表示 15 号最近的工作日#— 第几个星期几。比如3#2表示当月第二个星期二
我个人建议:别在线上直接手写 Cron 表达式。用在线生成器验证一下,或者写单元测试。我见过太多因为少写一个 ? 导致任务半夜乱跑的案例了。
3.3 持久化配置
默认情况下,Quartz 把任务信息存在内存里。一旦应用重启,所有调度信息就丢了。生产环境必须用持久化——把 Job 和 Trigger 信息存到数据库。
3.3.1 为什么需要持久化?
- 重启不丢任务:应用挂了再起来,调度信息还在
- 支持集群:多个节点共享数据库,避免重复调度
- 可追溯:可以查历史执行记录
3.3.2 配置步骤
第一步,引入依赖。如果你用 Spring Boot,加这个:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
第二步,创建 Quartz 数据库表。Quartz 官方提供了 11 张表的 SQL 脚本,在 docs/dbTables 目录下。我常用的表有:
| 表名 | 作用 |
|---|---|
QRTZ_JOB_DETAILS |
存储 Job 定义 |
QRTZ_TRIGGERS |
存储 Trigger 信息 |
QRTZ_CRON_TRIGGERS |
存储 CronTrigger 的表达式 |
QRTZ_SIMPLE_TRIGGERS |
存储 SimpleTrigger 的重复间隔 |
QRTZ_FIRED_TRIGGERS |
记录正在执行的 Trigger |
QRTZ_SCHEDULER_STATE |
集群模式下记录调度器状态 |
第三步,配置 quartz.properties:
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.dataSource.myDS.driver = com.mysql.cj.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/quartz_db
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = your_password
org.quartz.dataSource.myDS.maxConnections = 10
我的建议:如果使用 Spring Boot,可以直接在 application.yml 里配置,Quartz 会自动读取。但记得把 org.quartz.jobStore.isClustered 设为 true,即使你当前只有单节点——为以后扩容留个口子。
3.3.3 集群模式注意事项
Quartz 的集群是基于数据库锁实现的。每个调度器节点会定期更新 QRTZ_SCHEDULER_STATE 表,通过行级锁来抢任务。
- 时钟同步:集群节点的时间必须一致,否则会出现任务重复执行
- 数据库连接池:每个节点至少配置 3-5 个连接,避免锁等待超时
- 失败转移:如果一个节点挂了,其他节点会在
misfireThreshold时间后接管任务
我曾经遇到的坑:集群模式下,某个节点的系统时间慢了 2 分钟,结果那个节点一直抢不到任务,其他节点又以为它活着,导致任务整整延迟了 10 分钟才触发。所以,NTP 时钟同步是必须的。
3.4 知识体系总览
下面这张图把 Quartz 的核心概念和关系串起来了。你可以看到 Job、Trigger、Scheduler 如何协作,以及持久化层在其中的位置。
从图上你能看到,Scheduler 是中枢,它管理着 Job 和 Trigger 的配对。持久化层像是一个「黑匣子」,记录着所有调度状态。集群模式下,多个 Scheduler 节点通过数据库共享状态,实现高可用。
3.5 实战建议
最后,分享几个我这些年积累的经验:
- Job 类要无状态:所有参数通过 JobDataMap 传入,别用成员变量
- Cron 表达式先测试:用
TriggerUtils.computeFireTimes()验证下一次触发时间 - 持久化表要单独建库:别和业务表混在一起,方便维护
- misfireThreshold 设大一点:默认 60 秒,我一般设 120 秒,避免短暂网络抖动导致任务被跳过
- 监控 QRTZ_FIRED_TRIGGERS 表:如果这个表长时间有记录没清除,说明有任务卡住了
一句话记住:Job 是做什么,Trigger 是何时做,Scheduler 是统筹者。持久化是生产环境的底线,别偷懒。