Quartz 深度解析:核心组件与高可用实战
Quartz 这个框架,说实话,在 Java 调度领域算是老前辈了。我从 2012 年开始接触它,那时候还没那么多分布式调度框架可选。今天咱们就把它拆开看看,到底核心组件怎么配合、持久化怎么玩、集群模式怎么搭。
三大核心组件:Scheduler、Job、Trigger
Quartz 的设计其实很经典。它把调度拆成了三个角色:Scheduler(调度器)、Job(任务)、Trigger(触发器)。
我习惯这么理解:Scheduler 是老板,Job 是员工,Trigger 是排班表。老板按排班表叫员工干活。
Scheduler:调度中枢
Scheduler 是 Quartz 的门面。你所有操作——注册任务、启动、暂停、停止——都得通过它。
// 获取 Scheduler 实例
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
// 启动
scheduler.start();
// 关闭
scheduler.shutdown(true); // true 表示等待正在执行的任务完成
这里有个坑。我曾经在项目里直接调 shutdown(false),结果正在跑的任务被强行中断,数据库状态乱了。嗯,生产环境建议用 shutdown(true)。
Job:你要执行的任务
Job 就是一个接口,你实现 execute(JobExecutionContext context) 方法就行。
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 你的业务逻辑
System.out.println("任务执行时间:" + new Date());
}
}
注意,Quartz 每次执行 Job 都会创建一个新实例。所以 Job 里不要依赖成员变量存状态。你想想看,如果两个线程同时执行同一个 Job 定义,成员变量就乱套了。
Trigger:触发规则
Trigger 决定什么时候干活。Quartz 提供了两种主流 Trigger:
| Trigger 类型 | 适用场景 | 示例 |
|---|---|---|
| SimpleTrigger | 固定间隔、固定次数 | 每 5 秒执行一次,共 10 次 |
| CronTrigger | 复杂时间规则 | 每天 10:15 执行 |
// SimpleTrigger 示例
SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5)
.repeatForever())
.build();
// CronTrigger 示例
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity("trigger2", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 15 10 * * ?"))
.build();
我个人建议:能用 CronTrigger 就别用 SimpleTrigger。Cron 表达式可读性强,维护起来也方便。
JobStore 持久化:任务不丢的保障
Quartz 默认把任务信息存在内存里。一重启,全没了。生产环境必须用持久化 JobStore。
Quartz 支持两种持久化方式:
- RAMJobStore:内存存储,性能高但重启丢失
- JobStoreTX / JobStoreCMT:数据库存储,重启不丢
我遇到过最惨的一次事故:线上调度全用 RAMJobStore,半夜服务器重启,第二天所有定时任务都没了。从那以后,我强制团队所有项目必须用数据库 JobStore。
配置数据库 JobStore
# 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
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = root
org.quartz.dataSource.myDS.maxConnections = 10
Quartz 官方提供了 11 张表的建表脚本。核心表包括:
| 表名 | 作用 |
|---|---|
| QRTZ_JOB_DETAILS | 存储 Job 定义 |
| QRTZ_TRIGGERS | 存储 Trigger 信息 |
| QRTZ_CRON_TRIGGERS | CronTrigger 专用 |
| QRTZ_SIMPLE_TRIGGERS | SimpleTrigger 专用 |
| QRTZ_FIRED_TRIGGERS | 正在执行的 Trigger |
| QRTZ_LOCKS | 集群锁 |
集群模式与高可用
单机 Quartz 有单点故障风险。Quartz 集群模式通过数据库锁实现多节点协调。
集群原理
说白了就是:多个 Quartz 节点共享同一个数据库。谁抢到锁,谁就执行任务。
集群配置要点
开启集群只需要在 properties 里加几行配置:
# 开启集群
org.quartz.jobStore.isClustered = true
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.instanceName = MyClusteredScheduler
关键参数说明:
| 参数 | 说明 | 建议值 |
|---|---|---|
| isClustered | 开启集群模式 | true |
| instanceId | 节点唯一标识 | AUTO(自动生成) |
| checkinInterval | 节点心跳间隔(毫秒) | 20000(20秒) |
| misfireThreshold | 任务超时阈值(毫秒) | 60000(60秒) |
核心经验:集群节点数建议 2-3 个。太多节点反而增加数据库锁竞争,性能下降。我见过有人部署 10 个节点,结果大部分时间都在抢锁。
高可用保障
Quartz 集群的高可用靠的是「故障转移」。当一个节点挂了,其他节点会接管它的任务。
具体机制是这样的:
- 每个节点定期向数据库写心跳(checkin)
- 如果某个节点超过
checkinInterval没更新,就被判定为故障 - 其他节点会重新获取该节点持有的锁,执行未完成的任务
我曾经在生产环境遇到过一个问题:网络抖动导致节点被误判为故障,任务被重复执行。解决方案是适当调大 checkinInterval 和 misfireThreshold,给网络波动留点缓冲。
实战:搭建一个高可用 Quartz 集群
最后,我分享一个生产级的配置模板。这是我目前在用的,经过多次压测验证。
# 完整 quartz-cluster.properties
org.quartz.scheduler.instanceName = ProdScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.skipUpdateCheck = true
# 线程池配置
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
# JobStore 配置
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
org.quartz.jobStore.misfireThreshold = 60000
# 数据源
org.quartz.dataSource.qzDS.driver = com.mysql.cj.jdbc.Driver
org.quartz.dataSource.qzDS.URL = jdbc:mysql://db-host:3306/quartz?useSSL=false
org.quartz.dataSource.qzDS.user = quartz_user
org.quartz.dataSource.qzDS.password = your_password
org.quartz.dataSource.qzDS.maxConnections = 20
org.quartz.dataSource.qzDS.validationQuery = select 1
嗯,这套配置我跑了两年多,没出过因为 Quartz 本身导致的事故。当然,前提是数据库本身也要高可用。
Quartz 虽然老,但胜在稳定。理解它的核心组件和集群原理,你就能在生产环境里用好它。下一层,咱们可以聊聊怎么监控和调优这些调度任务。