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 如何协作,以及持久化层在其中的位置。

Quartz 核心架构图 Quartz 核心架构与数据流 Scheduler JobDetail - Job 类名 - JobDataMap 参数 - 分组与名称 Trigger - CronTrigger - SimpleTrigger - 优先级与 misfire 注册 绑定 execute() 执行 数据库持久化 读写 集群节点 2..N 💡 核心流程: 1. Scheduler 读取 JobDetail 和 Trigger 的绑定关系 2. 到达触发时间时,Scheduler 调用 Job 的 execute() 方法 3. 所有元数据通过 JDBC 持久化到数据库,支持集群和故障恢复

从图上你能看到,Scheduler 是中枢,它管理着 Job 和 Trigger 的配对。持久化层像是一个「黑匣子」,记录着所有调度状态。集群模式下,多个 Scheduler 节点通过数据库共享状态,实现高可用。

3.5 实战建议

最后,分享几个我这些年积累的经验:

  • Job 类要无状态:所有参数通过 JobDataMap 传入,别用成员变量
  • Cron 表达式先测试:用 TriggerUtils.computeFireTimes() 验证下一次触发时间
  • 持久化表要单独建库:别和业务表混在一起,方便维护
  • misfireThreshold 设大一点:默认 60 秒,我一般设 120 秒,避免短暂网络抖动导致任务被跳过
  • 监控 QRTZ_FIRED_TRIGGERS 表:如果这个表长时间有记录没清除,说明有任务卡住了

一句话记住:Job 是做什么,Trigger 是何时做,Scheduler 是统筹者。持久化是生产环境的底线,别偷懒。