Article

任务调度 Quartz

更新于:2026-07-15

第一章:Quartz 入门基础

1.1 Quartz 简介与核心特性

概念名称说明注意事项
Quartz 定义由 OpenSymphony 开源组织开发的 Java 作业调度框架,用于在指定时间或周期性执行任务。需通过实现 Job 接口定义任务逻辑。
核心能力支持 Cron 表达式、SimpleTrigger、任务持久化(内存/数据库)、集群部署、动态任务管理、监听器机制。生产环境建议使用 JDBCJobStore 实现高可用。
架构分层分为 API 层(Scheduler/Job/Trigger)、核心调度引擎、存储层(RAMJobStore / JDBCJobStore)。存储层选择直接影响性能与可靠性。
调度单位最小调度单位是 Job + Trigger 的组合,一个 Job 可被多个 Trigger 触发。JobDetail 是 Job 的”实例描述”,非 Job 本身。
并发控制默认允许多个 JobDetail 实例并发执行;可通过 @DisallowConcurrentExecution 注解禁止并发。该注解作用于 Job 类,对所有使用该类的 JobDetail 生效。

1.2 Quartz 与 Spring @Scheduled 对比

对比维度@Scheduled(Spring 原生)Quartz
依赖复杂度内置于 Spring Boot,无需额外依赖,仅需 @EnableScheduling需引入 quartzspring-boot-starter-quartz 依赖。
任务持久化不支持,任务信息仅存在于内存中,应用重启后丢失。支持 RAMJobStore(内存)和 JDBCJobStore(数据库),可持久化任务状态。
动态管理仅支持静态配置,修改 cron 表达式需重启应用。支持运行时动态添加、删除、暂停、恢复任务。
分布式支持多实例部署时任务会重复执行,需自行加分布式锁(如 Redis)。原生支持集群,通过数据库行锁保证任务仅在一个节点执行。
调度策略支持 fixedRatefixedDelay 和标准 Cron 表达式(秒级需自定义)。支持 SimpleTrigger(固定间隔)、CronTrigger(完整 7 段 Cron 表达式,含秒)。
错过触发处理无内置 misfire 处理机制。提供多种 Misfire 策略(如 FIRE_NOWDO_NOTHINGIGNORE_MISFIRE_POLICY)。
扩展性无监听器、插件机制。支持 JobListener、TriggerListener、SchedulerListener 及自定义插件。
适用场景单机、简单定时任务(如缓存刷新、日志清理)。企业级、复杂调度需求(如金融对账、分布式报表生成)。

1.3 快速搭建第一个 Quartz 应用

步骤名称操作细节注意事项
1. 添加 Maven 依赖pom.xml 中加入依赖若使用 Spring Boot,推荐使用 spring-boot-starter-quartz
<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>2.5.0</version>
</dependency>
步骤名称操作细节注意事项
2. 创建 Job 类实现 org.quartz.Job 接口,重写 execute 方法execute 方法不能有返回值,异常需抛出 JobExecutionException
public class HelloJob implements Job {
  public void execute(JobExecutionContext ctx) throws JobExecutionException {
    System.out.println("Hello Quartz! " + new Date());
  }
}
步骤名称操作细节注意事项
3. 构建 JobDetail使用 JobBuilder 创建任务描述withIdentity 指定任务名和组名,用于唯一标识。
JobDetail job = JobBuilder.newJob(HelloJob.class)
  .withIdentity("myJob", "group1")
  .build();
步骤名称操作细节注意事项
4. 创建 Trigger使用 TriggerBuilder 定义触发规则(例如每 5 秒执行)startNow() 表示立即开始;repeatForever() 表示无限循环。
Trigger trigger = TriggerBuilder.newTrigger()
  .withIdentity("myTrigger", "group1")
  .startNow()
  .withSchedule(SimpleScheduleBuilder.simpleSchedule()
    .withIntervalInSeconds(5).repeatForever())
  .build();
步骤名称操作细节注意事项
5. 启动 Scheduler获取调度器并启动必须调用 start() 才会真正开始调度;scheduleJob 绑定任务与触发器。
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
scheduler.scheduleJob(job, trigger);
步骤名称操作细节注意事项
6.(可选)保持主线程运行主方法末尾加 Thread.sleep() 防止程序退出否则程序可能在任务执行前就结束。
Thread.sleep(60_000); // 运行 1 分钟
scheduler.shutdown();

第二章:Quartz 核心组件详解

2.1 Job 与 JobDetail

概念/方法名称说明注意事项
Job 接口定义任务执行逻辑的接口,仅包含 execute(JobExecutionContext context) 方法。必须提供无参构造函数,Quartz 通过反射创建实例。
JobDetail描述 Job 的”实例模板”,包含 Job 类、名称、组、JobDataMap 等元数据。实际调度的是 JobDetail,而非 Job 实例本身。
JobKey由 name + group 构成,唯一标识一个 JobDetail。注册重复 JobKey 会抛出异常,除非显式覆盖。
JobBuilder.newJob(Class)静态方法,用于构建 JobDetail,传入 Job 实现类。示例:JobBuilder.newJob(MyJob.class).withIdentity("job1", "group1").build()
usingJobData(String, Object)向 JobDetail 的 JobDataMap 中添加键值对,供 execute 方法读取。值必须可序列化(如 String、Integer),否则持久化时会失败。
@DisallowConcurrentExecution注解,标记 Job 为”有状态”(等价于旧版 StatefulJob),禁止并发执行。多个 Trigger 触发同一 Job 时,会排队执行。
JobExecutionContext.getJobDetail()execute 方法中获取当前 JobDetail 实例。可通过 .getJobDataMap() 获取传递的参数。

示例代码:

public class PrintJob implements Job {
  public void execute(JobExecutionContext ctx) throws JobExecutionException {
    String msg = ctx.getJobDetail().getJobDataMap().getString("message");
    System.out.println("Message: " + msg);
  }
}
JobDetail job = JobBuilder.newJob(PrintJob.class)
  .withIdentity("printJob", "default")
  .usingJobData("message", "Hello from Quartz!")
  .build();

2.2 Trigger(SimpleTrigger 与 CronTrigger)

方法/属性名称语法用途代码示例注意事项
TriggerBuilder.newTrigger()静态工厂方法创建 Trigger 构建器TriggerBuilder.newTrigger().withIdentity("t1", "g1")所有 Trigger 均由此构建
startNow().startNow()立即开始触发.startNow()调度器 start() 后立即生效
startAt(Date).startAt(myDate)指定首次触发时间.startAt(new Date(System.currentTimeMillis() + 60_000))时间点必须在将来
endAt(Date).endAt(endTime)设置 Trigger 失效时间.endAt(dateOf(23, 0, 0))到达后不再触发,即使未达重复次数
withSchedule(SimpleScheduleBuilder).withSchedule(simpleSchedule().withIntervalInSeconds(10).repeatForever())定义 SimpleTrigger 调度规则见左列适用于固定间隔任务
withSchedule(CronScheduleBuilder).withSchedule(cronSchedule("0 0 9 * * ?"))定义 CronTrigger 调度规则.withSchedule(cronSchedule("0/5 * * * * ?"))支持 6 或 7 段 Cron 表达式(含秒)
withPriority(int).withPriority(10)设置 Trigger 优先级(默认 5).withPriority(8)仅在资源不足时影响调度顺序
usingJobData(String, Object).usingJobData("key", "value")向 Trigger 的 JobDataMap 添加参数.usingJobData("triggerSource", "manual")与 JobDetail 的 JobDataMap 合并传入 execute

SimpleTrigger 示例(每 3 秒执行一次,共 5 次):

Trigger trigger = TriggerBuilder.newTrigger()
  .withIdentity("simpleTrigger", "group1")
  .startNow()
  .withSchedule(SimpleScheduleBuilder.simpleSchedule()
    .withIntervalInSeconds(3)
    .withRepeatCount(4)) // 总共执行 5 次(含首次)
  .build();

CronTrigger 示例(每天上午 9:30 执行):

Trigger cronTrigger = TriggerBuilder.newTrigger()
  .withIdentity("dailyTrigger", "group1")
  .withSchedule(CronScheduleBuilder.cronSchedule("0 30 9 * * ?"))
  .build();

注意事项:

  • Cron 表达式格式:[秒] [分] [时] [日] [月] [周] [年(可选)]
  • SimpleTrigger 的 repeatCount=0 表示只执行一次;REPEAT_INDEFINITELY 表示无限循环。
  • Trigger 的 JobDataMap 与 JobDetail 的 JobDataMap 会合并,若 key 冲突,Trigger 的值优先。

2.3 Scheduler 调度器生命周期管理

方法名称语法用途代码示例注意事项
StdSchedulerFactory.getDefaultScheduler()Scheduler sched = StdSchedulerFactory.getDefaultScheduler();获取默认 Scheduler 实例见左列使用 quartz.properties 配置
scheduler.start()scheduler.start();启动调度器,开始执行任务scheduler.start();必须调用,否则任务不运行
scheduler.standby()scheduler.standby();暂停所有任务调度(可恢复)scheduler.standby();不释放线程池,仅暂停触发
scheduler.resumeAll()scheduler.resumeAll();恢复 standby 状态scheduler.resumeAll();仅对 standby 有效
scheduler.shutdown()scheduler.shutdown();关闭调度器,停止所有任务scheduler.shutdown();默认等待任务完成(可传 true 强制立即关闭)
scheduler.scheduleJob(JobDetail, Trigger)scheduler.scheduleJob(job, trigger);注册任务与触发器见左列一个 JobDetail 可绑定多个 Trigger
scheduler.unscheduleJob(TriggerKey)scheduler.unscheduleJob(triggerKey);删除指定 Triggerscheduler.unscheduleJob(TriggerKey.triggerKey("t1", "g1"));对应 JobDetail 若无其他 Trigger 则变为”孤立”
scheduler.deleteJob(JobKey)scheduler.deleteJob(jobKey);删除 JobDetail 及其所有关联 Triggerscheduler.deleteJob(JobKey.jobKey("job1", "g1"));彻底移除任务
scheduler.getContext()SchedulerContext ctx = scheduler.getContext();获取调度器上下文(全局共享 Map)ctx.put("globalConfig", config);所有 Job/Trigger 可通过 JobExecutionContext 访问

生命周期流程:

  1. 通过 SchedulerFactory 创建 Scheduler;
  2. 调用 start() 进入运行状态;
  3. 可随时 standby() 暂停、resumeAll() 恢复;
  4. 最终调用 shutdown() 结束生命周期。

注意事项:

  • Scheduler 是线程安全的,可在多线程环境中共享使用。
  • 调用 shutdown(true) 会中断正在执行的 Job 线程,需谨慎使用。
  • 在 Spring Boot 中,Scheduler 通常由容器管理,无需手动 shutdown。

第三章:Quartz 高级功能

3.1 JobDataMap 数据传递机制

方法/机制名称语法用途代码示例注意事项
JobDetail.usingJobData(String, Object)JobBuilder.newJob(...).usingJobData("name", "Alice")向 JobDetail 的 JobDataMap 添加参数见左列值必须可序列化(如 String、Integer、Float)
Trigger.usingJobData(String, Object)TriggerBuilder.newTrigger().usingJobData("source", "API")向 Trigger 的 JobDataMap 添加参数见左列用于区分不同 Trigger 触发同一 Job 的上下文
JobExecutionContext.getJobDetail().getJobDataMap()context.getJobDetail().getJobDataMap().getString("name")获取 JobDetail 中的参数见左列仅读取 JobDetail 定义的数据
JobExecutionContext.getTrigger().getJobDataMap()context.getTrigger().getJobDataMap().getString("source")获取 Trigger 中的参数见左列仅读取当前 Trigger 定义的数据
JobExecutionContext.getMergedJobDataMap()context.getMergedJobDataMap().getString("key")获取合并后的 JobDataMap(Trigger 优先)见左列若 key 冲突,Trigger 的值覆盖 JobDetail 的值
自动注入(Setter 注入)Job 类中定义 setName(String),Quartz 自动调用简化参数获取,无需手动 getXXX见下方示例JobFactory 必须支持(默认 StdJobFactory 支持);字段名需与 key 一致
put() / get() 方法dataMap.put("count", 1); int c = dataMap.getInt("count");存取任意类型数据(需强转)见左列推荐使用类型安全的 getXXX() 方法(如 getStringgetInt

自动注入示例:

public class MyJob implements Job {
  private String name;
  public void setName(String name) { this.name = name; }
  // ...
}

注意事项:

  • JobDataMap 实现了 java.util.Map<String, Object>,但建议使用其提供的类型安全方法。
  • 在 JDBCJobStore 模式下,所有存入 JobDataMap 的对象必须实现 Serializable,否则启动时报错。
  • 修改 JobDataMap 中的值(如计数器)在 RAMJobStore 中有效,但在 JDBCJobStore 中默认不会持久化回数据库,除非显式启用 @PersistJobDataAfterExecution(已废弃)或使用有状态 Job + 手动更新。

3.2 监听器(JobListener / TriggerListener / SchedulerListener)

JobListener

方法名称语法用途代码示例注意事项
getName()public String getName() { return "MyJobListener"; }返回监听器唯一名称见左列非全局监听器注册时需匹配此名称
jobToBeExecuted(JobExecutionContext)public void jobToBeExecuted(ctx) { ... }Job 即将执行前回调记录开始时间、前置校验可抛异常中断任务(但不推荐)
jobExecutionVetoed(JobExecutionContext)public void jobExecutionVetoed(ctx) { ... }Job 被 TriggerListener 否决时回调记录被拒绝原因仅当 TriggerListener.vetoJobExecution() 返回 true 时触发
jobWasExecuted(JobExecutionContext, JobExecutionException)public void jobWasExecuted(ctx, e) { ... }Job 执行完成后回调(无论成功失败)记录耗时、结果、异常处理可在此处重试或告警

注册方式:

// 全局监听器(监听所有 Job)
scheduler.getListenerManager().addJobListener(new MyJobListener());

// 非全局监听器(仅监听特定 Job)
scheduler.getListenerManager().addJobListener(new MyJobListener(),
    KeyMatcher.jobKeyEquals(JobKey.jobKey("myJob", "group1")));

TriggerListener

方法名称语法用途代码示例注意事项
getName()return "MyTriggerListener";返回监听器名称见左列非全局注册时需匹配
triggerFired(Trigger, JobExecutionContext)public void triggerFired(t, ctx) { ... }Trigger 触发后、Job 执行前回调记录触发时间可配合 veto 使用
vetoJobExecution(Trigger, JobExecutionContext)public boolean vetoJobExecution(t, ctx) { return true; }决定是否否决本次 Job 执行条件判断(如资源不足)返回 true 则 Job 不执行,触发 jobExecutionVetoed
triggerMisfired(Trigger)public void triggerMisfired(t) { ... }Trigger 错过触发时回调记录 misfire、补偿逻辑避免耗时操作,防止雪崩
triggerComplete(Trigger, JobExecutionContext, CompletedExecutionInstruction)public void triggerComplete(t, ctx, inst) { ... }Trigger 完成一次调度后回调清理资源、统计inst 表示后续行为(如是否重新调度)

注册方式:

scheduler.getListenerManager().addTriggerListener(new MyTriggerListener());

// 或绑定到特定 Trigger
scheduler.getListenerManager().addTriggerListener(listener, nameEquals("myTrigger"));

SchedulerListener

方法名称用途注意事项
jobScheduled(Trigger)新 Job 被调度时调用可用于监控任务注册
jobUnscheduled(TriggerKey)Job 被取消调度时调用
triggerFinalized(Trigger)Trigger 永久失效时调用(如 SimpleTrigger 达到 repeatCount
triggerPaused(TriggerKey) / triggersPaused(String group)Trigger 或组被暂停
triggerResumed(TriggerKey) / triggersResumed(String group)Trigger 或组恢复
jobAdded(JobDetail)JobDetail 被添加到 Scheduler
jobDeleted(JobKey)Job 被删除
schedulerError(String msg, SchedulerException cause)调度器发生严重错误重要!可用于告警
schedulerShutdown()调度器关闭时调用可用于清理资源

注册方式:

scheduler.getListenerManager().addSchedulerListener(new MySchedulerListener());

3.3 插件(Plugins)与日志记录

插件名称配置方式(quartz.properties用途注意事项
ShutdownHookPluginorg.quartz.plugin.shutdownHook.class = org.quartz.plugins.management.ShutdownHookPluginJVM 关闭时自动 shutdown Scheduler避免任务中断,确保优雅退出
org.quartz.plugin.shutdownHook.cleanShutdown = true
LoggingJobHistoryPluginorg.quartz.plugin.jobHistory.class = org.quartz.plugins.history.LoggingJobHistoryPlugin通过 SLF4J 记录 Job 执行历史(开始/结束)默认 INFO 级别,可调整日志级别控制输出
LoggingTriggerHistoryPluginorg.quartz.plugin.triggerHistory.class = org.quartz.plugins.history.LoggingTriggerHistoryPlugin记录 Trigger 触发历史(fire/misfire/complete)便于排查调度问题
自定义插件实现 org.quartz.spi.SchedulerPlugin 接口扩展初始化/销毁逻辑(如连接外部系统)initialize() 在 Scheduler 初始化后调用
在 properties 中声明

日志记录说明:

  • Quartz 使用 SLF4J 作为日志门面,需引入具体实现(如 logback、log4j2)。
  • 核心包日志类别:org.quartz.core(调度线程)、org.quartz.impl(工厂)、org.quartz.simpl(简单实现)。
  • 开启 DEBUG 日志可查看详细调度过程(如 Trigger 匹配、线程分配)。

示例:启用历史日志插件(quartz.properties):

org.quartz.plugin.jobHistory.class = org.quartz.plugins.history.LoggingJobHistoryPlugin
org.quartz.plugin.jobHistory.jobSuccessMessage = Job [{1}.{0}] finished at {2} with result: {3}
org.quartz.plugin.jobHistory.jobFailedMessage = Job [{1}.{0}] failed at {2}

注意事项:

  • 插件在 Scheduler 初始化阶段加载,顺序由配置决定。
  • 生产环境建议启用 ShutdownHookPlugin 防止任务丢失。
  • 日志插件可能产生大量日志,需合理设置日志级别和滚动策略。

第四章:Quartz 配置与持久化

4.1 quartz.properties 配置详解

配置项前缀属性名默认值用途注意事项
org.quartz.schedulerinstanceNameQuartzScheduler调度器实例名称,在集群中需全局一致用于区分多个调度器实例
instanceIdNON_CLUSTERED实例唯一 ID;设为 AUTO 可自动生成集群模式下必须唯一,建议设为 AUTO
threadNameinstanceName + "_QuartzSchedulerThread"主调度线程名称便于线程监控
idleWaitTime30000(毫秒)空闲时轮询触发器的等待时间不建议低于 5000ms,避免频繁 DB 查询
dbFailureRetryInterval15000(毫秒)数据库连接失败后的重试间隔仅在使用 JDBCJobStore 时生效
org.quartz.threadPoolclass无(必须显式设置)线程池实现类通常设为 org.quartz.simpl.SimpleThreadPool
threadCount无(必须显式设置)工作线程数量至少为 1;根据任务并发量调整,一般 10~50
threadPriority5NORMAL_PRIORITY线程优先级(1~10)不建议过高或过低
threadsInheritContextClassLoaderOfInitializingThreadtrue子线程是否继承初始化线程的 ClassLoader在应用服务器中建议设为 true
org.quartz.jobStoreclassorg.quartz.simpl.RAMJobStore作业存储实现类持久化需改为 org.quartz.impl.jdbcjobstore.JobStoreTX
misfireThreshold60000(毫秒)触发器错过执行的最大容忍时间超过则视为 misfire,进入 misfire 处理流程
tablePrefixQRTZ_数据库表前缀需与建表脚本一致
usePropertiesfalseJobDataMap 是否仅存字符串类型设为 true 可提升序列化兼容性
driverDelegateClass无(自动推断)数据库方言代理类MySQL 建议显式设为 StdJDBCDelegate
isClusteredfalse是否启用集群模式集群部署时必须设为 true
clusterCheckinInterval15000(毫秒)节点心跳上报间隔控制故障检测灵敏度
org.quartz.dataSource.[name]driverJDBC 驱动类名com.mysql.cj.jdbc.Driver
URL数据库连接 URLjdbc:mysql://localhost:3306/quartz?useSSL=false
user / password数据库账号密码
maxConnections最大连接数建议 ≥ threadCount

典型配置示例(JDBCJobStore + MySQL):

org.quartz.scheduler.instanceName = MyScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 20
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.dataSource = myDS
org.quartz.dataSource.myDS.driver = com.mysql.cj.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/quartz?useSSL=false&serverTimezone=UTC
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = password
org.quartz.dataSource.myDS.maxConnections = 25

4.2 RAMJobStore 与 JDBCJobStore 对比

对比维度RAMJobStoreJDBCJobStore
存储位置JVM 内存关系型数据库(如 MySQL、Oracle)
性能极高(内存访问)较低(依赖数据库 I/O)
持久性应用重启后所有任务丢失任务信息持久化,重启后自动恢复
集群支持不支持原生支持,通过数据库行锁保证单点执行
事务控制支持本地事务(JobStoreTX)或 JTA(JobStoreCMT)
适用场景单机、测试、临时任务生产环境、高可用、动态任务管理
配置复杂度极简(默认即用)需配置数据源、建表、调优
资源消耗仅占用堆内存需维护数据库连接、表结构
任务状态恢复无法恢复自动从 QRTZ_JOB_DETAILSQRTZ_TRIGGERS 等表加载
典型配置org.quartz.jobStore.class = org.quartz.simpl.RAMJobStoreorg.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

注意事项:

  • RAMJobStore 适合开发调试,但绝不应用于生产环境。
  • JDBCJobStore 需预先创建 Quartz 官方提供的 11 张表(如 QRTZ_JOB_DETAILSQRTZ_TRIGGERS 等)。
  • 在集群模式下,所有节点必须使用相同的 instanceName 和不同的 instanceId,并共享同一数据库。

4.3 使用数据库实现任务持久化

步骤名称操作细节注意事项
1. 添加依赖在 Maven 中加入 spring-boot-starter-quartz 以及数据库驱动(如 mysql-connector-java若非 Spring Boot 项目,需手动引入 quartz 和 HikariCP 等
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
步骤名称操作细节注意事项
2. 创建数据库与表执行 Quartz 官方 SQL 脚本(位于 quartz-x.x.jar/org/quartz/impl/jdbcjobstore/首次运行可设 initialize-schema: always,后续改为 never 防止表重建
  • MySQL: tables_mysql_innodb.sql
  • Oracle: tables_oracle.sql
步骤名称操作细节注意事项
3. 配置 application.yml见下方配置示例initialize-schema: always 仅首次使用,否则会清空任务表
spring:
  quartz:
    job-store-type: jdbc
    jdbc:
      initialize-schema: never # 第二次启动后必须为 never
    properties:
      org.quartz.scheduler.instanceName: QuartzScheduler
      org.quartz.scheduler.instanceId: AUTO
      org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
      org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
      org.quartz.threadPool.threadCount: 10
步骤名称操作细节注意事项
4. 创建持久化 Job见下方代码示例.storeDurably() 是关键,确保 JobDetail 存入数据库
JobDetail job = JobBuilder.newJob(MyJob.class)
  .withIdentity("job1", "group1")
  .storeDurably() // 必须!否则重启后丢失
  .build();

Trigger trigger = TriggerBuilder.newTrigger()
  .forJob(job)
  .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
  .build();

scheduler.scheduleJob(job, trigger);
步骤名称操作细节注意事项
5. 启用有状态 Job(可选)见下方代码示例用于需要保存 JobDataMap 修改状态的场景(如计数器)
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class StatefulJob implements Job { ... }
步骤名称操作细节注意事项
6. 验证持久化- 启动应用,确认任务执行
- 查看 QRTZ_JOB_DETAILS 表是否有记录
- 停止应用后重启,观察任务是否自动恢复
若未恢复,检查 storeDurably() 是否调用、initialize-schema 是否误设为 always

关键点总结:

  • .storeDurably():必须调用,否则 Job 被视为”临时”,不写入数据库。
  • initialize-schema: never:第二次及以后启动必须设置,防止 Quartz 重建表导致任务丢失。
  • 数据库表结构:共 11 张表,核心包括 QRTZ_JOB_DETAILS(任务定义)、QRTZ_TRIGGERS(触发规则)、QRTZ_FIRED_TRIGGERS(正在执行的任务)等。

第五章:Quartz 集群与分布式调度

5.1 集群原理与配置要点

概念/配置项说明注意事项
无中心架构Quartz 集群中所有节点地位平等,无主从之分,通过共享数据库协调任务执行。节点间不直接通信,仅通过数据库表感知彼此状态。
数据库行锁机制调度线程通过 SELECT ... FOR UPDATE 获取 QRTZ_LOCKS 表中的 TRIGGER_ACCESS 锁,确保同一时间仅一个节点扫描并获取待触发的 Trigger。数据库必须支持行级锁(如 InnoDB),MyISAM 不适用。
心跳检测每个节点定期更新 QRTZ_SCHEDULER_STATE 表中的 last_checkin_time,其他节点据此判断是否宕机。clusterCheckinInterval 控制心跳频率(默认 15 秒)。
故障转移当某节点宕机,其正在执行的任务(记录在 QRTZ_FIRED_TRIGGERS)会被其他节点检测到,并根据 REQUESTS_RECOVERY=true 标记决定是否重新执行。Job 必须设置 requestsRecovery = true 才能被恢复。
关键配置项见下方代码示例所有节点必须使用相同的 instanceName 和不同的 instanceId(推荐 AUTO)。
时钟同步要求所有集群节点系统时间必须基本一致(误差 < misfireThreshold)。时间不同步会导致任务漏触发或重复触发。
线程池独立性每个节点拥有独立的线程池,任务在其本地执行。总并发能力 = 节点数 × 单节点 threadCount

典型 quartz.properties 集群配置片段:

org.quartz.scheduler.instanceName = ClusteredScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.threadPool.threadCount = 15

5.2 分布式任务去重与高可用

机制名称实现方式用途注意事项
数据库行锁(核心)调度线程在获取待执行 Trigger 前,先锁定 QRTZ_LOCKS 表的 TRIGGER_ACCESS 行。确保同一 Trigger 仅被一个节点获取并执行。依赖数据库事务和行锁,是 Quartz 去重的根本保障。
Trigger 状态流转Trigger 状态从 WAITINGACQUIREDEXECUTINGCOMPLETE,状态变更由持有锁的节点完成。防止多个节点同时认为某个 Trigger 可执行。状态字段位于 QRTZ_TRIGGERS.trigger_state
Job 恢复标记在 JobDetail 中设置 .requestRecovery(true),当执行节点宕机,其他节点会重新执行该 Job。实现任务高可用,避免因节点故障丢失任务。仅对设置了 requestsRecovery=true 的 Job 生效;需 Job 本身幂等。
Misfire 处理若 Trigger 触发时间已过且超过 misfireThreshold,进入 misfire 流程,按策略处理(如立即执行、忽略)。应对节点长时间离线后重启的场景。可通过 withMisfireHandlingInstructionXXX() 自定义策略。
负载均衡多个节点竞争获取 Trigger,自然实现任务分发。避免单点过载,提升整体吞吐量。任务分配不保证绝对均匀,取决于调度时机。
避免重复注册动态添加任务时,应先检查 QRTZ_JOB_DETAILS 是否已存在同名 Job。防止多节点同时初始化导致重复任务。可通过 scheduler.checkExists(JobKey) 判断。

示例:创建可恢复的 Job:

JobDetail job = JobBuilder.newJob(RecoverableJob.class)
  .withIdentity("recoverJob", "group1")
  .requestRecovery(true) // 关键!启用故障恢复
  .storeDurably()
  .build();
// Job 实现需幂等
public class RecoverableJob implements Job {
  public void execute(JobExecutionContext ctx) throws JobExecutionException {
    // 例如:处理消息队列中的消息,需支持重复消费
  }
}

注意事项:

  • 幂等性:由于任务可能被重复执行(如恢复场景),Job 逻辑必须设计为幂等。
  • 网络分区:若数据库不可达,所有节点将停止调度,直至恢复。
  • 性能瓶颈:数据库成为集群性能瓶颈,需优化 DB 连接池、索引(如 QRTZ_TRIGGERS 表的 next_fire_time 索引)。

5.3 Terracotta 与 Quartz 集群集成(可选)

项目说明注意事项
Terracotta 是什么一种 JVM 级别的分布式内存网格(Distributed Shared Memory),可替代数据库实现无 DB 集群。由 Software AG 提供,商业产品(开源版已停止维护)。
Quartz-Terracotta 集成原理使用 TerracottaJobStore 将 Job/Trigger 状态存储在 Terracotta 服务器的共享内存中,节点通过 Terracotta 客户端通信。无需数据库,减少 I/O 开销,理论性能更高。
配置方式org.quartz.jobStore.class = org.terracotta.quartz.TerracottaJobStore
org.quartz.jobStore.tcConfigUrl = localhost:9510
需部署 Terracotta Server 并启动。
优势- 无数据库依赖
- 更低延迟
- 支持复杂对象存储(无需序列化限制)
劣势- 引入额外中间件(Terracotta Server)
- 社区支持弱
- 与主流云原生架构不兼容
官方已不推荐新项目使用
现状自 Quartz 2.0 起,Terracotta 集成由第三方维护;主流生产环境普遍采用 JDBCJobStore + MySQL/PostgreSQL。不建议在新项目中采用,除非有特殊历史原因。

替代方案建议:

  • 对于追求高性能、无 DB 的场景,可考虑 Elastic-Job、XXL-JOB 或 ShedLock + Spring @Scheduled
  • 对于云原生环境,推荐 Kubernetes CronJob + 幂等任务或 Quartz + RDBMS(成熟稳定)。

第六章:Quartz 与 Spring Boot 整合

6.1 spring-boot-starter-quartz 使用方式

配置/组件语法/说明用途代码示例注意事项
Maven 依赖org.springframework.boot:spring-boot-starter-quartz自动引入 Quartz 核心库及 Spring 集成支持见下方示例无需单独引入 quartz,版本由 Spring Boot 管理
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
配置/组件语法/说明用途代码示例注意事项
自动配置类QuartzAutoConfiguration(通过 spring.factories 加载)自动创建 SchedulerFactoryBean、Scheduler Bean无须手动配置可通过 @EnableAutoConfiguration(exclude = QuartzAutoConfiguration.class) 禁用
注入 Scheduler@Autowired private Scheduler scheduler;获取已初始化的调度器实例见下方示例Scheduler 已启动,可直接使用
@Service
public class JobService {
  @Autowired
  private Scheduler scheduler;
}
配置/组件语法/说明用途代码示例注意事项
Job 支持 Spring Bean 注入Job 类加 @Component,字段用 @Autowired在 Job 中使用 Service、Repository 等 Spring Bean见下方示例依赖 AutowiringSpringBeanJobFactory(Starter 默认启用)
@Component
public class MyJob implements Job {
  @Autowired
  private UserService userService;
  public void execute(...) { ... }
}
配置/组件语法/说明用途代码示例注意事项
application.yml 配置spring.quartz.* 命名空间覆盖默认 Quartz 属性(如线程池、存储类型)见下方示例所有 org.quartz.* 属性均可通过 properties 子项设置
spring:
  quartz:
    job-store-type: jdbc
    properties:
      org.quartz.threadPool.threadCount: 20
配置/组件语法/说明用途代码示例注意事项
禁用自动启动spring.quartz.auto-startup=false控制 Scheduler 是否在应用启动时自动 start()spring.quartz.auto-startup: false适用于需要延迟启动或条件启动的场景

关键机制说明:

  • SchedulerFactoryBean:Spring 封装的 FactoryBean,负责创建和管理 Scheduler 生命周期。
  • AdaptableJobFactory:将 Quartz Job 实例交由 Spring 容器管理,实现依赖注入。
  • 配置映射spring.quartz.properties.org.quartz.xxx → 最终写入 quartz.properties

6.2 动态添加/删除任务

操作类型方法用途代码示例注意事项
动态添加 Cron 任务scheduler.scheduleJob(JobDetail, Trigger)运行时注册新任务见下方完整示例Job 必须 storeDurably() 才能持久化
更新任务 Cron 表达式scheduler.rescheduleJob(TriggerKey, newTrigger)修改已有任务的调度时间scheduler.rescheduleJob(TriggerKey.triggerKey(oldName, group), newTrigger);推荐使用 rescheduleJob() 而非 delete+add,保留任务状态
暂停任务scheduler.pauseJob(JobKey)暂停指定 Job 的所有 Triggerscheduler.pauseJob(JobKey.jobKey("myJob", "group1"));任务状态变为 PAUSED,不触发但保留定义
恢复任务scheduler.resumeJob(JobKey)恢复已暂停的任务scheduler.resumeJob(JobKey.jobKey("myJob", "group1"));从暂停点继续调度
删除任务scheduler.deleteJob(JobKey)彻底移除 Job 及其所有 Triggerscheduler.deleteJob(JobKey.jobKey("myJob", "group1"));数据库中对应记录被清除
检查任务是否存在scheduler.checkExists(JobKey)判断 Job 是否已注册boolean exists = scheduler.checkExists(JobKey.jobKey("myJob", "group1"));避免重复添加

完整服务类示例:

@Service
public class QuartzDynamicService {

  @Autowired
  private Scheduler scheduler;

  public void addCronJob(String jobName, String group,
                          Class<? extends Job> jobClass, String cron)
                          throws SchedulerException {
    JobDetail job = JobBuilder.newJob(jobClass)
      .withIdentity(jobName, group)
      .storeDurably()
      .build();
    Trigger trigger = TriggerBuilder.newTrigger()
      .forJob(job)
      .withSchedule(CronScheduleBuilder.cronSchedule(cron))
      .build();
    scheduler.scheduleJob(job, trigger);
  }

  public void updateCronJob(String jobName, String group, String newCron)
                            throws SchedulerException {
    TriggerKey triggerKey = TriggerKey.triggerKey(jobName + "Trigger", group);
    CronTrigger newTrigger = TriggerBuilder.newTrigger()
      .withIdentity(triggerKey)
      .withSchedule(CronScheduleBuilder.cronSchedule(newCron))
      .build();
    scheduler.rescheduleJob(triggerKey, newTrigger);
  }

  public void deleteJob(String jobName, String group)
                        throws SchedulerException {
    scheduler.deleteJob(JobKey.jobKey(jobName, group));
  }
}

注意事项:

  • 动态任务若需持久化,必须使用 JDBCJobStore 并调用 .storeDurably()
  • rescheduleJob() 会保留原 Trigger 的状态(如 misfire 记录),优于 delete+add。
  • 所有操作均为线程安全,可在 Web 请求中直接调用。

6.3 Web 管理界面集成(如 Quartz Web UI)

方案实现方式优点缺点适用场景
自研 REST API + 前端提供 /jobs/list/jobs/pause/jobs/resume 等接口,前端调用完全可控,可深度定制开发成本高企业内部系统,需与现有平台集成
quartz-manager(第三方)引入开源项目快速获得可视化界面功能有限,维护状态不明快速原型验证
整合 XXL-JOB / ElasticJob使用更高级的调度框架替代 Quartz提供成熟 Web UI、权限控制、执行日志需迁移现有任务新项目或重构场景
Spring Boot Admin 扩展通过 Actuator Endpoint 暴露任务信息,Admin 展示与 Spring 生态无缝集成仅支持查看,不支持操作监控为主,操作通过 API
Swagger + Controller用 Swagger 文档化动态任务 API开发者友好,无需额外 UI无图形化操作界面内部调试或轻量级管理

自研 Web API 示例(Controller):

@RestController
@RequestMapping("/quartz")
public class QuartzController {

  @Autowired
  private QuartzDynamicService quartzService;

  @PostMapping("/job/add")
  public ResponseEntity<String> addJob(@RequestParam String name,
                                       @RequestParam String group,
                                       @RequestParam String cron) {
    try {
      quartzService.addCronJob(name, group, MyJob.class, cron);
      return ResponseEntity.ok("Added");
    } catch (Exception e) {
      return ResponseEntity.status(500).body(e.getMessage());
    }
  }

  @PostMapping("/job/delete")
  public ResponseEntity<String> deleteJob(@RequestParam String name,
                                          @RequestParam String group) {
    try {
      quartzService.deleteJob(name, group);
      return ResponseEntity.ok("Deleted");
    } catch (Exception e) {
      return ResponseEntity.status(500).body(e.getMessage());
    }
  }
}

推荐实践:

  • 对于简单需求,自研 REST API + Swagger 是最轻量高效的方式。
  • 若需生产级 UI,建议评估 XXL-JOB(国产、中文文档、活跃社区)而非强行扩展 Quartz。
  • 不要直接暴露 Scheduler 操作给前端,应封装为业务语义接口(如”开启每日报表”而非”addJob”)。

第七章:最佳实践与常见问题

7.1 任务幂等性与异常处理

机制/策略实现方式用途代码示例注意事项
业务唯一标识(Idempotency Key)在 JobDataMap 中传入唯一业务 ID(如订单号),执行前查 DB 是否已处理防止重复创建/扣款见下方示例唯一 ID 需由调用方生成并传递
String orderId = ctx.getMergedJobDataMap().getString("orderId");
if (orderService.exists(orderId))
  return; // 已处理,直接返回
机制/策略实现方式用途代码示例注意事项
数据库唯一索引在业务表上建立 (job_type, biz_id) 联合唯一索引利用 DB 层阻止重复插入见下方示例捕获 DuplicateKeyException 并静默处理
CREATE UNIQUE INDEX uk_job_order ON job_execution_log(job_type, order_id);
机制/策略实现方式用途代码示例注意事项
状态机校验执行前检查业务对象当前状态是否允许操作避免非法状态变更见下方示例适用于订单、支付等有明确状态流转的场景
Order order = orderDao.findById(orderId);
if (order.getStatus() != OrderStatus.CREATED)
  return; // 已支付,不重复处理
机制/策略实现方式用途代码示例注意事项
分布式锁(Redis)使用 Redisson 或 Lua 脚本加锁,确保同一任务仅一个实例执行防止集群下并发执行见下方示例锁粒度要细(如按订单 ID),避免全局锁
RLock lock = redisson.getLock("job:lock:" + jobId);
if (lock.tryLock(0, 30, SECONDS)) { ... }
机制/策略实现方式用途代码示例注意事项
Job 异常处理execute 方法中捕获异常,记录日志,决定是否抛出 JobExecutionException控制重试行为见下方示例JobExecutionExceptionrefireImmediately 参数控制是否重试
try { ... } catch (Exception e) {
  log.error("Job failed", e);
  throw new JobExecutionException(e, false); // false=不立即重试
}
机制/策略实现方式用途代码示例注意事项
启用 Misfire 处理Trigger 构建时指定 misfire 策略应对错过触发的情况见下方示例默认策略可能不符合业务预期,需显式设置
CronScheduleBuilder.cronSchedule(cron)
  .withMisfireHandlingInstructionDoNothing();

幂等性设计原则:

  • 核心思想:无论任务执行 1 次还是 N 次,最终业务状态一致。
  • 优先顺序:业务状态校验 > 唯一索引 > 分布式锁(成本递增)。
  • Quartz 本身不保证幂等,必须在 Job 逻辑中实现。

7.2 性能调优(线程池、数据库连接等)

调优维度配置项/参数推荐值说明注意事项
线程池大小org.quartz.threadPool.threadCountCPU 核数 × (1 + 平均等待时间/平均计算时间)控制并发任务数过小导致任务堆积,过大引发上下文切换开销;一般 10~100
数据库连接池HikariCP maximumPoolSizethreadCount + 5确保每个工作线程+调度线程有足够连接Quartz 调度线程也需 DB 连接(用于扫描 Trigger)
Trigger 扫描间隔org.quartz.scheduler.idleWaitTime10000~30000 ms调度线程空闲时休眠时间过小增加 DB 查询频率,过大延迟任务触发
Misfire 容忍阈值org.quartz.jobStore.misfireThreshold60000 ms(默认)触发器错过执行的最大容忍时间根据业务容忍度调整,过小导致频繁 misfire
批量获取 Triggerorg.quartz.jobStore.maxTriggersToAcquire1~5(默认 1)单次从 DB 获取的待触发 Trigger 数量增大可减少 DB 查询次数,但可能影响负载均衡
数据库索引优化QRTZ_TRIGGERS 表索引(next_fire_time, state)加速调度线程查询待触发任务必须存在,否则高并发下 DB CPU 飙升
禁用未使用功能移除监听器、插件减少回调开销如无需历史日志,禁用 LoggingJobHistoryPlugin

典型调优配置(application.yml):

spring:
  quartz:
    properties:
      org.quartz.threadPool.threadCount: 30
      org.quartz.scheduler.idleWaitTime: 15000
      org.quartz.jobStore.misfireThreshold: 120000
  datasource:
    hikari:
      maximum-pool-size: 40
      minimum-idle: 10

监控指标:

  • 线程池活跃线程数:接近 threadCount 表示任务堆积。
  • DB 查询 QPS:SELECT * FROM QRTZ_TRIGGERS WHERE next_fire_time < ? 频率过高需调大 idleWaitTime
  • 任务执行延迟:记录 triggerFiredjobToBeExecuted 时间差。

7.3 常见错误排查(如任务不触发、重复执行等)

问题现象可能原因排查步骤解决方案
任务完全不触发1. Scheduler 未 start()
2. Job 未调用 .storeDurably()(持久化模式)
3. Cron 表达式错误
1. 检查是否调用 scheduler.start()
2. 查看 QRTZ_JOB_DETAILS 表是否有记录
3. 用在线 Cron 工具验证表达式
1. 确保启动 Scheduler
2. 添加 .storeDurably()
3. 修正 Cron(注意 Quartz 支持秒段)
任务只执行一次1. SimpleTrigger 未设 repeatForever()
2. Job 抛出未捕获异常导致 Trigger 失效
1. 检查 Trigger 构建代码
2. 查看日志是否有 JobExecutionException
1. 添加 .repeatForever()
2. 捕获异常并决定是否重试
集群下任务重复执行1. 数据库表引擎非 InnoDB(无行锁)
2. 节点时间不同步
3. 未正确配置 isClustered=true
1. 执行 SHOW CREATE TABLE QRTZ_LOCKS
2. 检查各节点系统时间
3. 确认所有节点 quartz.properties 一致
1. 改为 InnoDB
2. 同步 NTP 时间
3. 统一配置文件
任务偶尔漏触发1. misfireThreshold 过小
2. 线程池满载,任务排队超时
1. 检查日志是否出现 “Handling misfired trigger”
2. 监控线程池活跃数
1. 增大 misfireThreshold
2. 增加 threadCount 或优化 Job 性能
动态添加任务失败1. JobKey 冲突
2. 未设置 storeDurably()
1. 捕获 ObjectAlreadyExistsException
2. 检查 QRTZ_JOB_DETAILS 是否残留旧任务
1. 先 checkExists() 再添加
2. 务必调用 .storeDurably()
应用重启后任务丢失1. initialize-schema: always
2. 使用 RAMJobStore
1. 检查 application.yml
2. 确认 job-store-type: jdbc
1. 改为 initialize-schema: never
2. 切换到 JDBCJobStore

通用排查工具:

  • 数据库查询
    • 查看待触发任务:SELECT * FROM QRTZ_TRIGGERS WHERE state='WAITING' ORDER BY next_fire_time;
    • 查看正在执行任务:SELECT * FROM QRTZ_FIRED_TRIGGERS;
  • 日志级别:开启 org.quartz=DEBUG 可查看详细调度过程。
  • 线程 Dump:若任务卡住,抓取线程栈分析是否死锁或阻塞。