
在处理定时任务如日终跑批、数据同步、报表生成时我们经常会面临一个痛点在微服务或多节点集群环境下传统的Scheduled注解会导致同一个任务在所有节点上重复执行。为了解决这个问题Quartz 的集群模式Cluster Mode成为了 Java 生态中最经典、最可靠的分布式调度解决方案之一。一、 Quartz 集群模式核心原理在单机环境下Quartz 将调度信息存储在内存中RAMJobStore。但在集群模式下Quartz 采用了“以数据库为中心的分布式协同”架构。Quartz Github仓库1. 共享数据库与分布式锁集群中的所有 Quartz 节点彼此之间不直接通信没有 RPC 或心跳网络而是通过共享同一个数据库JobStoreTX来感知彼此。当触发器Trigger到达执行时间时各个节点会去数据库“抢锁”。Quartz 利用数据库的行级悲观锁QRTZ_LOCKS表来保证同一时刻、同一个 Trigger 只能被一个节点成功获取并执行。2. 心跳机制与故障转移Failover心跳上报每个节点会定期默认 15 秒向QRTZ_SCHEDULER_STATE表写入自己的心跳时间戳。故障接管如果节点 A 宕机超过一定时间没有更新心跳节点 B 在扫描 Trigger 时会发现节点 A 已经“失联”。如果该任务配置了PersistJobDataAfterExecution或请求了恢复requestsRecoverytrue节点 B 会接管并重新执行节点 A 未完成的任务。二、 环境准备与数据库初始化1. 技术栈选型Java: 17Spring Boot: 3.2.xQuartz: 2.3.2 (Spring Boot 默认集成版本)数据库: MySQL 8.02. Maven 依赖在pom.xml中引入 Spring Boot 的 Quartz Starter 以及 MySQL 驱动dependencies!-- Spring Boot Web --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!-- Quartz Starter --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-quartz/artifactId/dependency!-- MySQL JDBC --dependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-j/artifactIdscoperuntime/scope/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-jdbc/artifactId/dependency/dependencies3. 初始化 Quartz 数据库表Quartz 集群必须依赖数据库表。你需要在 MySQL 中创建一个数据库如quartz_db并执行官方提供的 DDL 脚本。获取脚本路径你可以从 Quartz GitHub 仓库 下载tables_mysql_innodb.sql或者在 Maven 依赖包org/quartz/impl/jdbcjobstore/目录下找到它。执行脚本后你会看到 11 张以QRTZ_为前缀的表核心表包括QRTZ_JOB_DETAILSJob 的详细信息。QRTZ_TRIGGERS触发器信息。QRTZ_CRON_TRIGGERSCron 表达式信息。QRTZ_FIRED_TRIGGERS正在执行的触发器状态集群抢锁的核心。QRTZ_SCHEDULER_STATE节点心跳状态。QRTZ_LOCKS分布式悲观锁。三、 Spring Boot 核心配置在application.yml中进行 Quartz 集群配置。这里的配置是集群能否成功的关键spring:datasource:url:jdbc:mysql://localhost:3306/quartz_db?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghaiusername:rootpassword:yourpassworddriver-class-name:com.mysql.cj.jdbc.Driverquartz:job-store-type:jdbc# 必须使用 jdbc 持久化到数据库jdbc:initialize-schema:never# 表结构我们已经手动初始化设置为 never 防止重复建表properties:org:quartz:scheduler:instanceName:MyClusteredScheduler# 调度器名称所有节点必须一致instanceId:AUTO# 自动生成实例ID通常为主机名时间戳jobStore:class:org.springframework.scheduling.quartz.LocalDataSourceJobStore# 使用Spring管理的数据源driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegatetablePrefix:QRTZ_isClustered:true# 【核心】开启集群模式clusterCheckinInterval:15000# 心跳检查间隔毫秒默认15秒misfireThreshold:60000# Misfire错过触发的容忍阈值默认60秒threadPool:class:org.quartz.simpl.SimpleThreadPoolthreadCount:10# 调度线程池大小根据并发任务量调整threadPriority:5四、 实战代码编写1. 解决 Spring Bean 注入问题自定义 JobFactory默认情况下Quartz 通过反射实例化 Job这会导致 Job 类无法使用Autowired注入 Spring 容器中的 Service。我们需要自定义一个JobFactoryimportorg.quartz.spi.TriggerFiredBundle;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.config.AutowireCapableBeanFactory;importorg.springframework.scheduling.quartz.SpringBeanJobFactory;importorg.springframework.stereotype.Component;ComponentpublicclassAutowiringSpringBeanJobFactoryextendsSpringBeanJobFactory{AutowiredprivateAutowireCapableBeanFactorybeanFactory;OverrideprotectedObjectcreateJobInstance(TriggerFiredBundlebundle)throwsException{Objectjobsuper.createJobInstance(bundle);// 将 Job 实例纳入 Spring 容器管理支持 Autowired 注入beanFactory.autowireBean(job);returnjob;}}将其配置到SchedulerFactoryBean中importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.scheduling.quartz.SchedulerFactoryBean;importorg.springframework.scheduling.quartz.SpringBeanJobFactory;ConfigurationpublicclassQuartzConfig{BeanpublicSchedulerFactoryBeanCustomizerschedulerFactoryBeanCustomizer(SpringBeanJobFactoryautowiringSpringBeanJobFactory){returnbean-bean.setJobFactory(autowiringSpringBeanJobFactory);}}2. 编写 Job 任务注意在集群模式下Job 类必须实现Serializable接口因为需要在节点间传递或持久化并且强烈建议加上DisallowConcurrentExecution注解防止同一个 Job 定义被并发执行。importlombok.extern.slf4j.Slf4j;importorg.quartz.DisallowConcurrentExecution;importorg.quartz.Job;importorg.quartz.JobExecutionContext;importorg.quartz.JobExecutionException;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importjava.io.Serializable;importjava.time.LocalDateTime;Slf4jComponentDisallowConcurrentExecution// 禁止并发执行同一个 JobDefinitionpublicclassDataSyncJobimplementsJob,Serializable{// 验证 Spring Bean 是否成功注入AutowiredprivatetransientMyBusinessServicemyBusinessService;Overridepublicvoidexecute(JobExecutionContextcontext)throwsJobExecutionException{StringjobNamecontext.getJobDetail().getKey().getName();StringinstanceIdcontext.getScheduler().getSchedulerInstanceId();log.info(【Quartz集群】任务: {} 正在节点 [{}] 上执行, 时间: {},jobName,instanceId,LocalDateTime.now());// 调用业务逻辑myBusinessService.syncData();}}3. 编写动态调度服务 (Scheduler Service)在实际业务中我们通常需要通过后台界面动态管理任务而不是写死在代码里。importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.quartz.*;importorg.springframework.stereotype.Service;Slf4jServiceRequiredArgsConstructorpublicclassQuartzService{privatefinalSchedulerscheduler;/** * 创建并启动一个 Cron 任务 */publicvoidaddCronJob(Class?extendsJobjobClass,StringjobName,StringcronExpression)throwsSchedulerException{JobKeyjobKeyJobKey.jobKey(jobName);if(scheduler.checkExists(jobKey)){log.warn(任务 {} 已存在跳过创建,jobName);return;}// 构建 JobDetailJobDetailjobDetailJobBuilder.newJob(jobClass).withIdentity(jobKey).withDescription(动态创建的集群任务).requestRecovery(true)// 【核心】开启故障转移节点宕机后其他节点会接管.storeDurably(true).build();// 构建 CronTriggerCronScheduleBuilderscheduleBuilderCronScheduleBuilder.cronSchedule(cronExpression)// 设置 Misfire 策略错过的任务合并为一次立即执行然后按正常频率执行.withMisfireHandlingInstructionFireAndProceed();CronTriggertriggerTriggerBuilder.newTrigger().withIdentity(TriggerKey.triggerKey(jobName_trigger)).forJob(jobDetail).withSchedule(scheduleBuilder).build();scheduler.scheduleJob(jobDetail,trigger);log.info(成功创建任务: {}, Cron: {},jobName,cronExpression);}/** * 暂停任务 */publicvoidpauseJob(StringjobName)throwsSchedulerException{scheduler.pauseJob(JobKey.jobKey(jobName));}/** * 恢复任务 */publicvoidresumeJob(StringjobName)throwsSchedulerException{scheduler.resumeJob(JobKey.jobKey(jobName));}/** * 删除任务 */publicvoiddeleteJob(StringjobName)throwsSchedulerException{scheduler.deleteJob(JobKey.jobKey(jobName));}}五、 集群部署与验证1. 启动多个实例为了验证集群我们在本地启动两个 Spring Boot 实例可以通过 IDEA 的Allow parallel run功能或者打包成 Jar 后通过命令行指定不同端口启动Node A:java -jar app.jar --server.port8080Node B:java -jar app.jar --server.port80812. 触发任务并观察调用QuartzService.addCronJob(DataSyncJob.class, SyncJob_1, 0/10 * * * * ?);每10秒执行一次。观察控制台日志你会发现每 10 秒钟只有 Node A 或 Node B 其中的一个节点会打印执行日志绝不会同时打印。这就是数据库悲观锁在发挥作用。不清楚悲观锁看这篇乐观锁和悲观锁验证故障转移Failover将任务执行时间改长例如Thread.sleep(20000)模拟耗时 20 秒。在任务执行期间强制 Kill 掉正在执行该任务的节点进程。观察另一个存活节点的日志你会发现它检测到了节点失联并自动接管执行了该任务因为我们在JobDetail中设置了requestRecovery(true)。六、 进阶技巧在生产环境中使用 Quartz 集群必须注意以下几个核心问题1. 深入理解 Misfire错过触发机制什么是 Misfire如果到了 Trigger 的触发时间但由于线程池满、数据库卡顿、节点宕机重启等原因导致任务没有被按时触发且延迟时间超过了misfireThreshold默认 60 秒Quartz 就会判定为 Misfire。CronTrigger 的三大处理策略策略方法行为描述适用场景withMisfireHandlingInstructionFireAndProceed默认将错过的多次触发合并为一次立即执行后续按原计划执行。数据同步、状态更新不在乎中间遗漏的过程。withMisfireHandlingInstructionDoNothing忽略错过的触发直接等待下一次正常的 Cron 触发点。精准时间要求的报表生成如每天凌晨0点错过了就算了。withMisfireHandlingInstructionIgnoreMisfires强制补偿把过去错过的次数全部依次执行一遍。绝对不能漏执行的金融结算、订单超时取消。警告对于耗时任务慎用IgnoreMisfires否则可能导致节点重启后瞬间触发成百上千次任务直接压垮数据库。2. 长耗时任务的“线程池饥饿”问题Quartz 的调度线程池默认 10 个线程是全局共享的。如果你的某个 Job 需要执行 1 个小时它会一直占用这 10 个线程中的 1 个。如果有 10 个这样的长耗时任务并发整个 Quartz 调度器将被彻底阻塞导致其他简单的秒级任务也无法触发。解决方案方案 A异步化Job 的execute方法只负责投递消息到 RabbitMQ/Kafka或者提交给 Spring 的Async线程池让 Job 瞬间执行完毕释放调度线程。方案 B分离线程池针对长耗时任务在业务代码内部使用自定义的ThreadPoolExecutor执行但要注意控制并发量。3. 任务的幂等性设计虽然 Quartz 的数据库锁能保证“同一时刻只有一个节点执行”但在极端网络分区或数据库主从延迟的情况下仍存在极小概率的重复执行风险。专业规范所有的定时任务业务逻辑必须设计为幂等的。例如通过数据库唯一索引、Redis 分布式锁、或者基于业务状态机如UPDATE orders SET status PROCESSED WHERE id ? AND status PENDING来兜底。4. 架构选型Quartz vs XXL-JOB作为专业开发者我们需要知道何时使用 Quartz何时使用 XXL-JOB 等新一代调度中心维度Quartz 集群XXL-JOB / PowerJob架构去中心化依赖数据库行锁中心化调度中心 执行器RPC数据库压力节点多、任务多时高频抢锁会导致数据库 CPU 飙升调度中心内存计算对数据库压力极小可视化管理无需自己开发后台界面调用 API自带完善的 Web 控制台、日志监控、滚动实时日志路由策略随机抢锁轮询、一致性Hash、故障转移、分片广播等适用场景轻量级、不想引入额外中间件、强依赖本地事务企业级微服务、海量任务调度、需要可视化运维结论如果你的系统已经有完善的微服务基建且任务量庞大万级别推荐使用XXL-JOB如果你的系统相对独立任务量在百级别以内且希望零额外组件部署Quartz 集群依然是王者。七、 总结Quartz 集群模式通过共享数据库 悲观锁 心跳机制优雅地解决了分布式环境下的任务调度一致性问题。掌握以下核心要点足以让你应对 95% 以上的 Quartz 开发场景必须使用JobStoreTX(JDBC)并正确初始化 11 张表。通过自定义JobFactory打通 Quartz 与 Spring IoC 容器的壁垒。深刻理解并根据业务选择正确的Misfire 策略。警惕线程池饥饿长耗时任务必须异步化。永远不要信任框架的绝对可靠性业务幂等性是最后的防线。