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 定义,成员变量就乱套了。

我的习惯:把 Job 做成无状态的。需要参数?通过 JobDataMap 传进去。

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 集群锁
注意:表前缀可以自定义,但建议用默认的 QRTZ_。我曾经改过前缀,结果脚本没跑全,少了几张表,排查了半天。

集群模式与高可用

单机 Quartz 有单点故障风险。Quartz 集群模式通过数据库锁实现多节点协调。

集群原理

说白了就是:多个 Quartz 节点共享同一个数据库。谁抢到锁,谁就执行任务。

Quartz 集群架构 Quartz 节点 1 Scheduler + JobStore Quartz 节点 2 Scheduler + JobStore Quartz 节点 3 Scheduler + JobStore 共享数据库 QRTZ_LOCKS 表 + 任务表 集群锁机制 节点通过 QRTZ_LOCKS 表行锁竞争 抢到锁的节点执行任务,其他节点等待

集群配置要点

开启集群只需要在 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 集群的高可用靠的是「故障转移」。当一个节点挂了,其他节点会接管它的任务。

具体机制是这样的:

  1. 每个节点定期向数据库写心跳(checkin)
  2. 如果某个节点超过 checkinInterval 没更新,就被判定为故障
  3. 其他节点会重新获取该节点持有的锁,执行未完成的任务

我曾经在生产环境遇到过一个问题:网络抖动导致节点被误判为故障,任务被重复执行。解决方案是适当调大 checkinInterval 和 misfireThreshold,给网络波动留点缓冲。

避坑指南:Quartz 集群不保证任务「恰好一次」执行。它只能保证「至少一次」。如果你的业务要求严格幂等,必须在 Job 里自己实现去重逻辑。

实战:搭建一个高可用 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 虽然老,但胜在稳定。理解它的核心组件和集群原理,你就能在生产环境里用好它。下一层,咱们可以聊聊怎么监控和调优这些调度任务。