Quartz:调度线程如何把时间变成一次 Job 执行
Quartz 是嵌入 Java 应用进程的任务调度库。应用提供要执行的 Job,用 Trigger 描述时间规则,再由 Scheduler 管理注册、暂停、恢复、执行和停止。Quartz 不强制要求独立的调度中心:最小形态只是当前 JVM、一个线程池和内存中的 RAMJobStore;需要持久化或多节点协同时,可以换成 JDBC JobStore,让数据库保存 Job、Trigger、执行现场和节点心跳。
框架内部可以分成五层。公开 API 层负责构造 Job、Trigger、Calendar 和监听器;StdSchedulerFactory 把属性配置装配成运行时组件;QuartzSchedulerThread 负责寻找即将到期的 Trigger;JobRunShell 把一次触发包裹成完整的监听、执行和回写生命周期;JobStore 与 ThreadPool 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 桩和可运行示例。
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 | 定义 Scheduler、Job、Trigger、Calendar、监听器和 Builder | 应用代码面向这些接口编程,具体运行时隐藏在 core 与 impl 后面 |
StdSchedulerFactory | 读取属性,反射创建 ThreadPool、JobStore、JobFactory、插件和监听器 | 是配置文件与运行时对象图之间的装配入口 |
QuartzScheduler | 实现调度核心、状态管理、事件通知和资源释放 | StdScheduler 对外委托给 QuartzScheduler,QuartzScheduler 再调用 JobStore、线程池和监听器 |
QuartzSchedulerThread | 等待可用工作线程、获取即将到期的 Trigger、创建执行壳 | 上游接收调度变更信号,下游把 JobRunShell 交给 ThreadPool |
JobRunShell | 创建 Job 实例、通知监听器、执行、解释结果并回写 | 把一次 Trigger 触发变成完整的 Job 生命周期 |
RAMJobStore | 在单进程内存中保存 Job、Trigger、Calendar 和状态 | 启动简单、速度快,但停止进程后状态消失,也不提供跨节点协调 |
JobStoreSupport | JDBC 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. 获取同一份源码
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. 构建核心模块
./gradlew clean build该命令会解析远程依赖并执行构建配置中的测试,不能在离线环境直接假设成功。只想先编译核心库时,可缩小为:
./gradlew :quartz:classes3. 运行最小示例
./gradlew :examples:runExample1Example 1 使用默认 RAMJobStore 和 SimpleThreadPool,把一个 HelloJob 安排到下一个整分钟,启动后等待 65 秒,再调用 shutdown(true)。可观察信号包括:
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 和集群参数。示意配置如下,凭据与连接池大小需要按环境调整:
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.initialize | org/quartz/quartz.properties、系统属性覆盖逻辑 |
| 组件怎样被创建 | StdSchedulerFactory.instantiate | QuartzSchedulerResources、反射属性注入 |
| Job 与 Trigger 怎样入库 | QuartzScheduler.scheduleJob | JobStore.storeJobAndTrigger |
| 到期任务怎样被发现 | QuartzSchedulerThread.run | JobStore.acquireNextTriggers |
| Job 实例怎样创建 | JobRunShell.initialize | JobFactory.newJob、JobExecutionContextImpl |
| 业务方法怎样执行 | JobRunShell.run | 监听器通知、Job.execute、executionComplete |
| 内存状态怎样流转 | RAMJobStore | TriggerWrapper、timeTriggers、blockedJobs |
| 数据库锁和状态怎样流转 | JobStoreSupport | StdJDBCDelegate、Constants、QRTZ 表 |
| misfire 怎样处理 | applyMisfire / MisfireHandler | 各 *TriggerImpl.updateAfterMisfire |
| 集群节点故障怎样恢复 | ClusterManager | doCheckin、clusterRecover、QRTZ_SCHEDULER_STATE |
| 监听与 veto | ListenerManagerImpl | QuartzScheduler 的 notify 方法、Matcher 实现 |
| XML 或停机扩展 | SchedulerPlugin | plugins/xml、ShutdownHookPlugin |
从工厂到可运行调度器
入口不是 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.scheduleJob | Builder → Trigger 首次时间 → JobStore → 调度线程信号 | RAM 或数据库 | Job/Trigger 可被后续获取 |
| 到期获取 | QuartzSchedulerThread.run | 可用线程数 → acquireNextTriggers → 等待准确时刻 | 系统时钟、JobStore | Trigger 进入 ACQUIRED |
| Job 执行 | triggersFired | FiredBundle → JobFactory → JobRunShell → WorkerThread | 业务代码及其资源 | 执行完成指令与运行耗时 |
| 完成回写 | triggeredJobComplete | Trigger 指令、JobDataMap、并发解锁 → JobStore | RAM 或数据库事务 | WAITING、COMPLETE、ERROR 或删除 |
| misfire | RAM 获取路径或 JDBC MisfireHandler | 阈值判断 → Trigger 策略 → 新 nextFireTime | 系统时间、数据库锁(JDBC) | 立即补一次、跳过或完成 |
| 监听与否决 | JobRunShell.notifyListenersBeginning | TriggerListener → 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 预取任务。可用线程出现后,调度线程按空闲数、maxBatchSize 和 batchTimeWindow 获取一批 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 根据异常生成 CompletedExecutionInstruction。RE_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、批获取窗口配置 |
| 创建 Job | Job 类缺失或 JobFactory 失败 | 当前 Trigger 的相关状态被设为错误 | classpath、JobFactory 日志、Trigger ERROR |
| 监听器 | TriggerListener veto | 不执行 Job,但仍完成 Trigger 状态推进 | veto 日志、Listener Matcher 范围 |
| Job 异常 | 抛 JobExecutionException | Trigger 解释 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 创建与依赖注入 | JobFactory、SimpleJobFactory | Job 是否每次新建、容器作用域、异常处理 |
| 增加时间语义 | OperableTrigger、现有 TriggerImpl | next/previous fire time、Calendar、misfire、序列化 |
| 接入新存储 | JobStore SPI | 并发状态、原子获取、完成回写、恢复和信号器 |
| 排查任务没执行 | QuartzSchedulerThread | 是否 start、线程池空闲、Trigger 状态、nextFireTime |
| 排查重复执行 | QRTZ_FIRED_TRIGGERS | instanceId、requestsRecovery、业务幂等键、集群时钟 |
| 排查错过时间 | MisfireHandler | threshold、Trigger misfire 指令、线程池占用、数据库锁 |
| 观察指定任务 | ListenerManagerImpl | Matcher 是否覆盖正确 JobKey/TriggerKey,监听器耗时 |
| 配置式装载任务 | XMLSchedulingDataProcessorPlugin | overwrite/ignoreDuplicates、文件扫描与集群限制 |
应用场景与同类方案对比
Quartz 适合任务逻辑已经在 Java 应用中,希望得到比定时线程更完整的时间表达、持久化、监听、misfire 和可选数据库集群能力的场景。Quartz 尤其适合“调度与业务在同一 JVM 生命周期内”的系统,例如定期结算、报表、缓存刷新和设备轮询。
如果需要跨语言 Worker、独立控制台、权限审批、远程日志、DAG 编排或长事务恢复,Quartz 只解决了其中的时间调度部分,需要额外平台能力。
| 方案 | 主要定位 | 状态与执行位置 | 更合适的取舍 |
|---|---|---|---|
| Quartz | Java 嵌入式调度库 | RAM/JDBC 保存调度状态,Job 在应用 JVM 执行 | 需要丰富时间语义和库级控制,愿意自行建设管理面 |
ScheduledExecutorService | JDK 内进程定时执行 | 主要是内存线程池 | 任务少、时间规则简单、不需要持久化和集群 |
Spring @Scheduled | Spring 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 与集群恢复线程。
继续阅读可以按问题进入:想理解时间推进,从 CronTriggerImpl 和 SimpleTriggerImpl 开始;想排查“到点没跑”,沿 QuartzSchedulerThread → JobStore.acquireNextTriggers 走;想理解一次执行的异常和重试,从 JobRunShell 进入;想评估集群可靠性,则同时阅读 JobStoreSupport、DriverDelegate、QRTZ 表和 RecoverJobs 测试。扩展任何 SPI 前,先把正常路径、失败回写和停机恢复一起画出来,避免只实现“能运行”的一半契约。
参考资料
- quartz-scheduler/quartz GitHub 仓库
- Quartz 官方文档
- Quartz 2.5.x 配置参考
- Quartz 2.5.2 Release
- 固定源码提交 bb2505a
- Introduction to Quartz — 适合先建立 Job、Trigger、Scheduler 与监听器的使用语义,示例不替代当前源码链路。
- DolphinScheduler 源码分析 10:Quartz — 从其他调度系统集成 Quartz 的视角解释启动与获取流程,分析基于较早版本。
- Quartz Scheduler Misfire Instructions Explained — 对比 Simple/Cron misfire 行为,常量和边界应以 2.5.x 源码为准。
- Persisting Quartz Scheduler Jobs in a Database — 补充 JDBC JobStore 的配置与表结构入口,部署参数需按实际数据库调整。
- Scheduling one-time jobs with Quartz using JDBC JobStore in Spring — 展示一次性持久任务与重启场景,Spring 和 Quartz 配置属于历史版本写法。