Skip to content

Quartz:调度线程如何把时间变成一次 Job 执行

Quartz 是嵌入 Java 应用进程的任务调度库。应用提供要执行的 Job,用 Trigger 描述时间规则,再由 Scheduler 管理注册、暂停、恢复、执行和停止。Quartz 不强制要求独立的调度中心:最小形态只是当前 JVM、一个线程池和内存中的 RAMJobStore;需要持久化或多节点协同时,可以换成 JDBC JobStore,让数据库保存 Job、Trigger、执行现场和节点心跳。

框架内部可以分成五层。公开 API 层负责构造 Job、Trigger、Calendar 和监听器;StdSchedulerFactory 把属性配置装配成运行时组件;QuartzSchedulerThread 负责寻找即将到期的 Trigger;JobRunShell 把一次触发包裹成完整的监听、执行和回写生命周期;JobStoreThreadPool SPI 则隔离状态存储和实际工作线程。数据库、JTA、RMI、JMX、应用服务器和业务资源都在这些边界之外,按配置接入。

Quartz 最值得理解的地方并不是 Cron 语法,而是“时间规则”和“执行状态”被拆开:Trigger 计算下一次时间,JobStore 负责竞争与状态转换,调度线程只在有可用工作线程时取任务,JobRunShell 最后才创建 Job 实例并调用业务代码。新增一个更早的 Trigger 会唤醒正在等待的调度线程;Job 执行失败也不会简单等同于 Trigger 永久失败,而是由 CompletedExecutionInstruction 决定重跑、删除或修改后续状态。

本文固定阅读提交 bb2505a,重点覆盖默认装配、调度生命周期、RAM/JDBC 两种状态路径、misfire、并发限制、监听器、插件和 JDBC 集群恢复。示例、附加 Job 包和远程管理只用于说明边界,不声称逐文件审计整个仓库。

GitHub 仓库信息

信息内容
项目标题Quartz Scheduler
项目描述一个可嵌入 Java 应用的任务调度库,支持多种时间规则、内存或数据库持久化、监听器、插件以及基于共享数据库的集群协调。
GitHub 仓库quartz-scheduler/quartz
官网地址Quartz Scheduler
主要开发语言Java 99.08%、TSQL 0.80%、HTML 0.12%
开源许可证Apache-2.0
最近代码更新2026-08-24(GitHub pushed_at,核对日期:2026-09-03)

仓库结构与模块职责

仓库是一个 Gradle 多模块工程。真正决定调度行为的是 quartz;其余模块分别提供现成 Job、编译期 API 桩和可运行示例。

text
quartz/
├── quartz/                         # 核心库
│   ├── src/main/java/org/quartz/   # Scheduler、Job、Trigger、Builder 等公开 API
│   ├── core/                       # 调度器、调度线程、JobRunShell、信号器
│   ├── impl/                       # 标准工厂、Scheduler 门面、JDBC JobStore、Trigger 实现
│   ├── simpl/                      # RAMJobStore、SimpleThreadPool、默认 JobFactory
│   ├── spi/                        # JobStore、ThreadPool、Plugin 等扩展协议
│   ├── plugins/                    # 历史日志、中断监控、XML 装载、停机钩子
│   └── src/main/resources/         # 默认 quartz.properties、XSD、各数据库建表 SQL
├── quartz-jobs/                    # 文件扫描、邮件、JMS、JMX、EJB 等可复用 Job
├── quartz-stubs/                   # 部分应用服务器和事务 API 的编译桩
├── examples/                       # Simple、Cron、misfire、监听器、集群等 15 组示例
├── docs/                           # 指向官网版本化文档的入口
├── build.gradle                    # 全局发布、签名、Java toolchain 与仓库配置
├── settings.gradle                 # 四个 Gradle 子项目
└── gradle.properties               # 当前源码版本和依赖版本
模块或核心文件负责什么与上下游的关系
org.quartz API定义 SchedulerJobTrigger、Calendar、监听器和 Builder应用代码面向这些接口编程,具体运行时隐藏在 coreimpl 后面
StdSchedulerFactory读取属性,反射创建 ThreadPool、JobStore、JobFactory、插件和监听器是配置文件与运行时对象图之间的装配入口
QuartzScheduler实现调度核心、状态管理、事件通知和资源释放StdScheduler 对外委托给 QuartzScheduler,QuartzScheduler 再调用 JobStore、线程池和监听器
QuartzSchedulerThread等待可用工作线程、获取即将到期的 Trigger、创建执行壳上游接收调度变更信号,下游把 JobRunShell 交给 ThreadPool
JobRunShell创建 Job 实例、通知监听器、执行、解释结果并回写把一次 Trigger 触发变成完整的 Job 生命周期
RAMJobStore在单进程内存中保存 Job、Trigger、Calendar 和状态启动简单、速度快,但停止进程后状态消失,也不提供跨节点协调
JobStoreSupportJDBC JobStore 的事务、锁、Trigger 获取、misfire 与集群恢复骨架通过 DriverDelegate 适配数据库差异,通过 QRTZ 表共享状态
SimpleThreadPool预创建固定数量的 WorkerThread 并进行同步交接调度线程先确认空闲数,再提交 JobRunShell
quartz-jobs提供邮件、JMS、文件扫描和远程调用等现成 Job属于可选业务适配,不参与核心调度循环

核心能力与实现机制

Job 与 Trigger 解耦:工作内容不绑定时间规则

JobDetail 保存 Job 类、标识、持久数据以及并发和恢复属性;Trigger 保存开始时间、结束时间、优先级、Calendar、misfire 指令和下一次触发时间。一个持久 Job 可以没有 Trigger,也可以关联多个 Trigger。调用 scheduleJob(jobDetail, trigger) 时,核心先校验两者关联,计算首次触发时间,再一起写入 JobStore,并通知调度线程可能出现了更早的候选时间。

这层分离让“每天凌晨执行”和“失败后十分钟补跑”能够指向同一份工作定义,也让暂停 Trigger 不必删除 Job。代价是状态模型更丰富:排障时需要分别检查 Job 是否存在、Trigger 是否处于可执行状态、下一次时间是否为空,以及 Calendar 是否排除了该时间。

多种时间模型:每个 Trigger 自己推进下一次时间

Quartz 内建 Simple、Cron、CalendarInterval 和 DailyTimeInterval 等 Trigger。调度线程不会解析 Cron;QuartzSchedulerThread 只读取 nextFireTime。真正触发时,triggered(calendar) 把当前的 nextFireTime 移到 previousFireTime,再计算后续时间,并不断跳过 Calendar 排除的时刻。

这意味着自定义 Trigger 的关键契约并不是“能解析一种表达式”,而是能够稳定维护前一次、当前和下一次时间,并正确实现 misfire 与执行完成后的状态转换。若 nextFireTime 意外为空,JDBC 获取逻辑会记录警告并跳过,无法替应用修复错误数据。

可替换的状态层:RAM 和 JDBC 共用一套 JobStore 协议

默认配置使用 RAMJobStore。RAMJobStore 以 Map、Set 和单个内存锁维护对象与状态,获取 Trigger 时从按时间排序的集合取出候选;遇到 @DisallowConcurrentExecution,同一批次只允许一个同 Job 的 Trigger 被获取,实际开始执行后还会把该 Job 的其他 Trigger 标记为 BLOCKED。

JDBC JobStore 把同样的抽象映射为数据库状态:WAITING → ACQUIRED → EXECUTING,完成后回到 WAITING、进入 COMPLETE/ERROR,或被删除。获取阶段用条件更新把 WAITING 改成 ACQUIRED,并写入 QRTZ_FIRED_TRIGGERS;提交失败时还会用 fired record 验证操作是否已经落库。共享数据库因此既是持久化层,也是集群竞争与故障恢复的事实来源。

misfire:错过计划时间后重新解释,而不是盲目补齐

misfire 指 Trigger 的下一次时间已经早于“当前时间减去阈值”。默认阈值是 60 秒。RAMJobStore 在获取候选时就检查;JDBC JobStore 另有 MisfireHandler 后台线程,先轻量计数,再取得 TRIGGER_ACCESS 锁,批量读取并调用各 Trigger 的 updateAfterMisfire

不同 Trigger 的策略并不相同。Cron 的 SMART_POLICY 会转成 FIRE_ONCE_NOW;DO_NOTHING 则跳到未来第一个合法时间。SimpleTrigger 还要结合重复次数,决定立即执行、保留总次数,还是扣除已经错过的次数。misfire 因此是业务时间语义,不能只靠全局阈值调优。

并发、状态数据与恢复是三个独立开关

@DisallowConcurrentExecution 以 JobKey 为粒度阻止同一 JobDefinition 并发;该注解不代表“整个 Job 类全局单线程”。@PersistJobDataAfterExecution 决定成功执行后是否把变更过的 JobDataMap 写回。requestRecovery 则决定节点故障时是否为尚未完成的执行创建恢复 Trigger。

把三者分开很重要:禁止并发不代表自动持久化数据,请求恢复也不保证业务操作恰好一次。数据库记录可以帮助 Quartz 重新调度,但业务侧数据库、HTTP 调用或文件写入仍需要幂等键、事务或补偿策略。

监听器、插件和 SPI:扩展点分布在不同生命周期

JobListener 观察 Job 执行前后,TriggerListener 可在触发前 veto,SchedulerListener 观察调度器级事件;Matcher 让监听器只订阅指定 JobKey 或 TriggerKey。插件则拥有 initialize、start、shutdown 生命周期,可以装载 XML、注册停机钩子、记录历史或监控长时间 Job。

更底层的 JobStore、ThreadPool、JobFactory、ClassLoadHelper 和 ThreadExecutor 属于 SPI。替换这些组件会改变状态一致性、对象创建或线程语义,风险明显高于增加一个监听器。StdSchedulerFactory 通过属性反射和 Bean setter 注入配置,因此拼错属性或类不在 ClassLoader 中会在装配期失败。

源码环境准备与本地运行

固定源码的根构建声明 Java toolchain 11,Gradle Wrapper 使用 8.5。gradle.properties 在这个提交中写的是 2.5.1-SNAPSHOT;GitHub 最新公开 Release 则是 2025 年 12 月 1 日发布的 v2.5.2。二者不能混写成同一个版本,复现本文链路应以 commit 为准。

本次没有执行 Gradle、测试、示例、数据库脚本或仓库中的任何代码。下面命令来自固定构建文件和示例任务,成功信号来自示例源码,不是本次亲测结果。

1. 获取同一份源码

bash
git clone https://github.com/quartz-scheduler/quartz.git
cd quartz
git checkout bb2505aa340b4f36c7da164e4d34cafd1249e21c
git status --short --branch

最后一条应显示 detached HEAD 或指向该提交,且没有意外修改。准备 JDK 11 或允许 Gradle toolchain 自动找到兼容 JDK。

2. 构建核心模块

bash
./gradlew clean build

该命令会解析远程依赖并执行构建配置中的测试,不能在离线环境直接假设成功。只想先编译核心库时,可缩小为:

bash
./gradlew :quartz:classes

3. 运行最小示例

bash
./gradlew :examples:runExample1

Example 1 使用默认 RAMJobStoreSimpleThreadPool,把一个 HelloJob 安排到下一个整分钟,启动后等待 65 秒,再调用 shutdown(true)。可观察信号包括:

text
Initialization Complete
Started Scheduler
Hello World!
Shutdown Complete

只有看见 Hello World! 才说明 Trigger 已经真正进入 Job 执行;“Scheduler started”只证明调度器离开 standby。

4. 切换到 JDBC JobStore

源码为 PostgreSQL、MySQL、MariaDB、Oracle、SQL Server、DB2、H2、Derby 等数据库提供建表脚本。以 PostgreSQL 为例,先执行 tables_postgres.sql,再配置 DataSource、DriverDelegate 和集群参数。示意配置如下,凭据与连接池大小需要按环境调整:

properties
org.quartz.scheduler.instanceName = AppScheduler
org.quartz.scheduler.instanceId = AUTO

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
org.quartz.jobStore.dataSource = appDS
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true

org.quartz.dataSource.appDS.driver = org.postgresql.Driver
org.quartz.dataSource.appDS.URL = jdbc:postgresql://127.0.0.1:5432/app
org.quartz.dataSource.appDS.user = quartz
org.quartz.dataSource.appDS.password = <secret>
org.quartz.dataSource.appDS.maxConnections = 15

表结构、前缀和 Delegate 必须配套。启用集群后,所有节点还必须使用同一组 QRTZ 表,并为每个实例生成唯一 instanceId。

整体架构地图与功能分布

想找的能力源码入口继续向下读
默认配置从哪里来StdSchedulerFactory.initializeorg/quartz/quartz.properties、系统属性覆盖逻辑
组件怎样被创建StdSchedulerFactory.instantiateQuartzSchedulerResources、反射属性注入
Job 与 Trigger 怎样入库QuartzScheduler.scheduleJobJobStore.storeJobAndTrigger
到期任务怎样被发现QuartzSchedulerThread.runJobStore.acquireNextTriggers
Job 实例怎样创建JobRunShell.initializeJobFactory.newJobJobExecutionContextImpl
业务方法怎样执行JobRunShell.run监听器通知、Job.executeexecutionComplete
内存状态怎样流转RAMJobStoreTriggerWrappertimeTriggersblockedJobs
数据库锁和状态怎样流转JobStoreSupportStdJDBCDelegateConstants、QRTZ 表
misfire 怎样处理applyMisfire / MisfireHandler*TriggerImpl.updateAfterMisfire
集群节点故障怎样恢复ClusterManagerdoCheckinclusterRecoverQRTZ_SCHEDULER_STATE
监听与 vetoListenerManagerImplQuartzScheduler 的 notify 方法、Matcher 实现
XML 或停机扩展SchedulerPluginplugins/xmlShutdownHookPlugin

从工厂到可运行调度器

入口不是 main,而是宿主应用取得 Scheduler

Quartz 是库,通常没有唯一进程入口。最常见入口是 new StdSchedulerFactory().getScheduler(),框架集成也可能传入 Properties,或直接使用 DirectSchedulerFactory 编程装配。默认工厂会先在工作目录找 quartz.properties,再找指定的 classpath 资源,最后回退到 jar 内置配置;JVM 系统属性会覆盖文件值。

装配链

一个容易误读的细节是:QuartzScheduler 构造时已经把 QuartzSchedulerThread 交给 ThreadExecutor,但调度线程初始处于 paused。真正调用 scheduler.start() 后,JobStore 才收到 schedulerStarted(),JDBC 模式由此启动 ClusterManager 和 MisfireHandler;插件随后 start,最后调度线程解除暂停。

停机顺序也有明确边界:先进入 standby,停止调度线程,可选中断实现了 InterruptableJob 的当前任务,再关闭工作线程池、JMX/RMI、插件和 JobStore。shutdown(true) 等待的是 Quartz 已接管的执行线程,不代表外部业务系统事务一定完成。

核心链路矩阵

链路入口关键交接外部依赖最终结果
注册调度Scheduler.scheduleJobBuilder → Trigger 首次时间 → JobStore → 调度线程信号RAM 或数据库Job/Trigger 可被后续获取
到期获取QuartzSchedulerThread.run可用线程数 → acquireNextTriggers → 等待准确时刻系统时钟、JobStoreTrigger 进入 ACQUIRED
Job 执行triggersFiredFiredBundle → JobFactory → JobRunShell → WorkerThread业务代码及其资源执行完成指令与运行耗时
完成回写triggeredJobCompleteTrigger 指令、JobDataMap、并发解锁 → JobStoreRAM 或数据库事务WAITING、COMPLETE、ERROR 或删除
misfireRAM 获取路径或 JDBC MisfireHandler阈值判断 → Trigger 策略 → 新 nextFireTime系统时间、数据库锁(JDBC)立即补一次、跳过或完成
监听与否决JobRunShell.notifyListenersBeginningTriggerListener → veto → JobListener用户监听器代码执行、跳过或监听器错误
JDBC 集群恢复ClusterManager.doCheckin节点心跳 → 失败实例识别 → fired records 清理/恢复共享数据库解锁 Trigger,按请求创建恢复执行

一次 Trigger 的完整执行链

1. 入库时先计算,不是等到调度线程再解析

scheduleJob 先验证 Job、Trigger 和关联键,读取可选 Calendar,调用 computeFirstFireTime。若永远不会触发,方法立即失败;成功后才写入 JobStore。新 Trigger 的时间通过 signalSchedulingChange 通知调度线程,避免调度线程仍按旧的更晚时间睡眠。

2. 线程池容量决定一次取多少 Trigger

调度线程先调用 blockForAvailableThreads()。默认 SimpleThreadPool 在没有空闲 Worker 时阻塞,因此 Quartz 不会无限制地从 JobStore 预取任务。可用线程出现后,调度线程按空闲数、maxBatchSizebatchTimeWindow 获取一批 Trigger。

这形成一层直接背压:工作线程满时,Trigger 留在 JobStore,而不是先进入无界执行队列。若工作线程长期被慢任务占满,后续任务会逐渐成为 misfire;加线程只能缓解 CPU 或 I/O 并发,不能修复阻塞业务代码或数据库连接不足。

3. “获取”和“触发”是两次状态确认

JDBC 获取先把 WAITING 条件更新为 ACQUIRED,并插入 fired record。到目标时间后,triggersFired 再确认 Trigger 仍处于 ACQUIRED,加载 Job 和 Calendar,把 fired record 更新为 EXECUTING,并调用 Trigger 推进下一次时间。

两阶段确认处理了等待期间的变化:Trigger 可能被暂停、删除或因更早任务到来而释放。RAMJobStore 也检查包装状态;失败时调度线程调用 releaseAcquiredTrigger,把仍可用的 Trigger 放回候选集合。

4. JobRunShell 是执行边界,不只是 Runnable

JobRunShell 初始化时通过 JobFactory 创建新的 Job 实例,并生成 JobExecutionContext。执行时先通知 TriggerListener;监听器可以 veto,随后通知 JobListener,再调用 job.execute(context)。普通 Throwable 会被包装成 JobExecutionException 并通知 SchedulerListener,避免工作线程直接丢失异常。

Job 返回后,Trigger 根据异常生成 CompletedExecutionInstructionRE_EXECUTE_JOB 会在同一个 JobRunShell 循环中增加 refireCount 并再次执行;其他指令再回到 JobStore,决定 Trigger 删除、完成、错误或正常等待。这里没有通用的退避队列,业务若要求延迟重试,通常应创建新的 Trigger 或在更高层治理。

5. 完成回写释放并发限制

使用 @DisallowConcurrentExecution 时,JobStore 在开始执行前阻塞同 JobKey 的其他 Trigger,完成后再把 BLOCKED 恢复为 WAITING。使用 @PersistJobDataAfterExecution 时,只在 JobDataMap 被标记为 dirty 后更新数据库。最后删除 fired record,调度线程才看到完整闭环。

RAM、JDBC、misfire 与集群分支

RAMJobStore:一个锁内完成状态变换

RAMJobStore 的调度集合按下一次时间和优先级排序。获取时移除最早 Trigger,应用 misfire,检查批次内的并发限制,标为 ACQUIRED 后返回克隆。开始触发后推进下一次时间,并在必要时阻塞同 Job 的所有 Trigger;执行完成再恢复。

RAMJobStore 的优势是依赖少、定位直观;代价是进程退出即丢失调度数据,所有竞争都在单 JVM 锁内,也无法让另一个节点接替。开发、测试和可重建任务适合从这里开始,关键生产调度则要认真评估持久性要求。

JDBC JobStore:数据库同时保存定义、计划与执行现场

QRTZ_JOB_DETAILS 保存 Job 属性,QRTZ_TRIGGERS 保存通用 Trigger 时间和状态,各具体 Trigger 表保存 Cron 或重复间隔等专有字段;QRTZ_FIRED_TRIGGERS 保存已获取/执行中的现场,QRTZ_SCHEDULER_STATE 保存节点心跳,QRTZ_LOCKS 提供命名锁。

集群模式会强制使用数据库锁。Trigger 获取、misfire、完成回写和故障恢复都围绕 TRIGGER_ACCESS 或 STATE_ACCESS 运行。JDBC 集群提供的是“多个 Quartz 节点共享同一调度状态”的能力,不是任意跨地域分布式工作流:数据库延迟、锁竞争、时钟偏差和连接池容量会直接影响触发时效。

节点故障恢复

ClusterManager 周期更新自己的 check-in 时间,识别超过容忍窗口或留下孤儿 fired record 的实例。恢复时释放失败节点持有的 ACQUIRED/BLOCKED 状态,删除其 fired records;只有 Job 明确 requestsRecovery,才创建位于 RECOVERING_JOBS 组的一次性恢复 Trigger,并把原 Trigger 名、组和时间写入 JobDataMap。

恢复 Trigger 仍然可能再次执行已经产生外部副作用的 Job。Quartz 只能确认自己的执行记录没有完成,无法知道远端 HTTP 是否已经成功、数据库事务是否已经提交。因此恢复能力必须与业务幂等设计一起使用。

misfire 的三个常见误区

  • misfireThreshold 不是允许误差的 SLA,而是“多晚才进入错过策略”的判断窗口。
  • SMART_POLICY 会按 Trigger 类型解释,不是一条全局统一规则。
  • 集群恢复和 misfire 是两套机制:前者处理节点遗留的执行现场,后者处理计划时间已经落后。

失败、降级与扩展边界

阶段失败或分支源码行为运维观察点
配置装配类名错误、属性 setter 不存在、DataSource 未配置抛 SchedulerConfigException,已初始化资源进入清理初始化异常、实际加载的 properties 来源
获取 Trigger数据库暂时不可用调度线程增加失败计数并延迟重试SchedulerListener error、数据库连接与锁等待
等待触发新增更早的 Trigger调度变更信号唤醒并可能释放已获取批次nextFireTime、批获取窗口配置
创建 JobJob 类缺失或 JobFactory 失败当前 Trigger 的相关状态被设为错误classpath、JobFactory 日志、Trigger ERROR
监听器TriggerListener veto不执行 Job,但仍完成 Trigger 状态推进veto 日志、Listener Matcher 范围
Job 异常抛 JobExecutionExceptionTrigger 解释 refire、unschedule 等标志refireCount、异常标志、最终 Trigger 状态
线程池满没有空闲 Worker调度线程在获取前等待活跃线程、任务耗时、misfire 数量
JDBC 完成回写失败数据库连接中断retryExecuteInNonManagedTXLock 按间隔重试直到停机fired record、数据库恢复时间、监听器错误
集群节点失联check-in 过期其他节点清理状态;按 requestsRecovery 决定是否补执行QRTZ_SCHEDULER_STATE、QRTZ_FIRED_TRIGGERS

中断也有清楚限制。Scheduler.interrupt 只处理当前 Scheduler 实例中实现了 InterruptableJob 的对象,源码明确说明该中断 API 不具备集群感知;Java interrupt 仍依赖业务代码响应。强制停止外部进程、跨节点取消和分布式锁释放不在核心能力内。

仓库中存在 Management REST 相关配置和接口,但 QuartzScheduler.initialize() 中启动内嵌 REST 服务的代码在固定提交里被整体注释。不能因为类和属性存在,就将 Management REST 写成已启用的核心管理面。JMX 和 RMI 路径仍有活动代码,但生产暴露需要单独处理认证、网络和序列化风险。

值得学习的设计与不足

值得学习

  • 时间计算与状态竞争分开。 Trigger 负责时间语义,JobStore 负责谁能执行,调度线程只做协调,因此 RAM 与 JDBC 可以共用上层运行时。
  • 获取前先看执行容量。 默认线程池没有空闲 Worker 时不预取 Trigger,避免“状态已经占用、执行却长期排队”的扩大效应。
  • 调度变化是显式信号。 新任务、完成解锁和恢复动作都能唤醒调度线程,减少固定轮询造成的迟到。
  • 执行生命周期集中在 JobRunShell。 veto、监听、异常包装、重执行和完成回写有一个共同边界,扩展和排障不必散落到每种 JobStore。
  • JDBC 恢复保留执行现场。 fired record 同时承载节点、Job、Trigger、状态和恢复标志,使失败节点清理有可复查依据。

需要带着条件看

  • SPI 灵活性带来配置复杂度。 JobStore、ThreadPool、Delegate、连接池和事务模式组合很多,错误常在运行装配或特定数据库路径才暴露。
  • 数据库集群依赖共享锁和时间判断。 高延迟、锁竞争或节点时钟偏差会影响到期获取和失败节点判定,不等同于无中心协调系统。
  • 恢复不是恰好一次。 requestsRecovery 会再次调度未完成记录,但不能原子覆盖业务系统副作用。
  • 默认线程池模型较传统。 固定 WorkerThread 加同步交接容易理解,但没有现代执行器常见的队列、拒绝策略和细粒度指标面。
  • 管理能力较轻。 核心库提供 JMX、RMI、监听器和插件,不提供类似独立调度平台的完整权限、审批、租户、可视化日志与远程 Worker 治理。
  • 历史兼容面较宽。 JTA、RMI、应用服务器桩、不同数据库 Delegate 和序列化对象增加了维护与安全审查范围。

扩展与排障入口

目标建议起点同时检查
自定义 Job 创建与依赖注入JobFactorySimpleJobFactoryJob 是否每次新建、容器作用域、异常处理
增加时间语义OperableTrigger、现有 TriggerImplnext/previous fire time、Calendar、misfire、序列化
接入新存储JobStore SPI并发状态、原子获取、完成回写、恢复和信号器
排查任务没执行QuartzSchedulerThread是否 start、线程池空闲、Trigger 状态、nextFireTime
排查重复执行QRTZ_FIRED_TRIGGERSinstanceId、requestsRecovery、业务幂等键、集群时钟
排查错过时间MisfireHandlerthreshold、Trigger misfire 指令、线程池占用、数据库锁
观察指定任务ListenerManagerImplMatcher 是否覆盖正确 JobKey/TriggerKey,监听器耗时
配置式装载任务XMLSchedulingDataProcessorPluginoverwrite/ignoreDuplicates、文件扫描与集群限制

应用场景与同类方案对比

Quartz 适合任务逻辑已经在 Java 应用中,希望得到比定时线程更完整的时间表达、持久化、监听、misfire 和可选数据库集群能力的场景。Quartz 尤其适合“调度与业务在同一 JVM 生命周期内”的系统,例如定期结算、报表、缓存刷新和设备轮询。

如果需要跨语言 Worker、独立控制台、权限审批、远程日志、DAG 编排或长事务恢复,Quartz 只解决了其中的时间调度部分,需要额外平台能力。

方案主要定位状态与执行位置更合适的取舍
QuartzJava 嵌入式调度库RAM/JDBC 保存调度状态,Job 在应用 JVM 执行需要丰富时间语义和库级控制,愿意自行建设管理面
ScheduledExecutorServiceJDK 内进程定时执行主要是内存线程池任务少、时间规则简单、不需要持久化和集群
Spring @ScheduledSpring Bean 方法的声明式定时随应用实例执行接入成本最低;复杂动态调度、持久状态和节点协调需另做
XXL-JOB独立调度中心 + 业务执行器中心数据库管理任务,远程触发 Executor需要控制台、执行器注册、远程日志、重试和人工治理
Temporal持久化工作流编排服务端保存工作流历史,Worker 执行业务活动需要跨步骤可靠恢复、长事务与工作流语义,系统复杂度也更高

选择的分水岭不在 Cron 功能多少。若任务与应用同部署、只需时间驱动,Quartz 很贴近问题;若调度要成为团队共享平台,XXL-JOB 一类产品更完整;若真正需要的是可恢复业务流程,应评估工作流引擎,而不是继续给 Trigger 叠加职责。

构建、部署与调试问题

Scheduler 已创建,但任务一直不执行

确认是否调用了 scheduler.start()。工厂返回 Scheduler 时调度线程已经创建,但保持暂停。再检查 Trigger 的 nextFireTime、Calendar、standby 状态和工作线程是否全部被占用。成功标准是 Job 日志或 Listener 收到执行事件,不只是初始化日志。

切换 JDBC 后提示 DataSource 或表不存在

检查 org.quartz.jobStore.dataSource 是否与 org.quartz.dataSource.<name> 一致,建表脚本是否适配数据库,tablePrefix 是否与实际表名一致,DriverDelegate 是否选对。多个应用共库时还要避免误用同一 scheduler.instanceName 和表集合。

同一个 Job 仍然并发执行

确认 @DisallowConcurrentExecution 标注在实际 Job 类上,并检查是否使用了不同 JobKey。限制按 JobDetail 的 key 生效,不按 Java 类全局生效。集群模式还需要所有节点共享同一 JDBC JobStore;RAMJobStore 只能约束本进程。

服务恢复后突然集中执行很多任务

这是 misfire 策略的结果,不一定是重复调度。检查 Trigger 类型、显式 misfire instruction、全局 threshold 和停机时长。对不可补跑的任务使用 DO_NOTHING;对只需补一次的 Cron 任务可选择 FIRE_AND_PROCEED。修改前先评估业务幂等和下游容量。

集群节点退出后任务长期 BLOCKED

查看 QRTZ_SCHEDULER_STATE 的 instanceId 与 check-in,确认其他节点的 ClusterManager 正常运行、数据库锁可取得、各节点时钟差异可控。再检查 QRTZ_FIRED_TRIGGERS 是否仍有失败节点记录。不要直接删表中记录而不确认 Trigger 状态与正在运行的实例。

停机等待很久

shutdown(true) 会等待已交给工作线程的 Job 完成。若 Job 阻塞在外部 I/O,先检查超时和中断响应;实现 InterruptableJob 只提供协作式中断。需要硬隔离的任务应放到独立进程或远程 Worker,不应依赖 JVM 内线程强杀。

XML 配置刷新在集群中表现不一致

XMLSchedulingDataProcessorPlugin 的源码说明周期文件扫描不支持集群环境。集群中应使用统一的部署流程或外部配置治理,避免每个节点独立扫描并覆盖同一组 Job/Trigger。

总结与后续阅读路径

Quartz 的主线可以压缩成一句话:工厂把配置装配成 Scheduler、JobStore 和 ThreadPool;调度线程只在存在执行容量时获取到期 Trigger;JobRunShell 完成监听、Job 调用和状态回写;Trigger 维护时间,JobStore 维护竞争与恢复。RAMJobStore 把调度状态留在一个 JVM,JDBC JobStore 则把调度状态搬到共享数据库,并增加 misfire 与集群恢复线程。

继续阅读可以按问题进入:想理解时间推进,从 CronTriggerImplSimpleTriggerImpl 开始;想排查“到点没跑”,沿 QuartzSchedulerThread → JobStore.acquireNextTriggers 走;想理解一次执行的异常和重试,从 JobRunShell 进入;想评估集群可靠性,则同时阅读 JobStoreSupport、DriverDelegate、QRTZ 表和 RecoverJobs 测试。扩展任何 SPI 前,先把正常路径、失败回写和停机恢复一起画出来,避免只实现“能运行”的一半契约。

参考资料

文章结论绑定 source commit;动态信息以核对日期为准。