Spring Boot集成Quartz:从@Scheduled到动态集群调度实践
发布时间:2026/10/15 22:48:37 锦皓数字建站

1. 为什么我在项目里用Quartz替换掉Spring自带的Scheduled一提到定时任务Spring全家桶用户的第一反应往往是Scheduled。这个注解确实简单写一个方法加个注解就行项目里的小需求完全够用。可一旦任务数量上来了、业务开始复杂它那几个天生短板就会变成硬伤。最典型的三点运行状态全在内存里。应用一重启任务就没了下次触发时间重新算没法保证“错过了就补”这种需求。不能在运行期动态调整。CRON表达式是写死在代码里的想改成每分钟一次只能改代码重新发版。没有集群感知。多个实例同时跑同一个任务会每台机器各执行一次没有分布式锁的概念。我参与过一个内部运营系统里面大概有二十几个定时任务从凌晨的数据汇总到每个整点的缓存刷新都有。初期用Scheduled完全没问题可后来增加了一个“运营人员在前台页面直接新建定时任务输入CRON表达式绑定某个处理逻辑”的需求这就尴尬了——因为每次CRON表达式变化都得改代码任务的新增删除也无法热生效更别提找个地方把任务历史执行记录存下来。于是才认真考虑上Quartz。这个东西在Java领域算是老牌调度库了和Spring没有任何绑定关系纯Java就能跑。但它的核心设计很有针对性把“干什么”和“什么时候干”彻底分开。Job是任务本体Trigger是触发规则Scheduler是总指挥。这个模型天然支持把规则存进数据库、在运行期修改规则、多个节点协同调度。Spring Boot从2.0开始还专门做了spring-boot-starter-quartz这个starter把Bean装配的活儿基本做完了接入成本比早年直接写Quartz原生配置低了一大截。这篇就完整记录一下我这套整合过程从依赖配置到持久化再到现在能动态增删任务、支撑集群部署的方案。如果你现在的项目还在用Scheduled而且已经出现了“任务越来越多、改动越来越频繁、跑挂了没人知道”的苗头可以考虑直接切过来。实际上在正式引入Quartz之前我顺手验证过一个更轻的方案——在Scheduled之外加一张任务配置表启动的时候反射扫一遍需要调度的方法再配合动态代理去刷新调度周期。做了一半发现这套东西其实就是在重造Quartz的轮子定时触发要自己算、并发控制要自己写、持久化要自己搞。于是果断止损老老实实接Quartz。2. 最小可用整合依赖、Bean配置与第一个Job跑通2.1 依赖引入与Spring Boot版本的处理Spring Boot 2.x和3.x都能用这个starter。我用的是Spring Boot 2.7.x配合Quartz 2.3.2starter里自带的版本。如果用的是Spring Boot 3.xQuartz会升级到2.4.x以上API层面基本兼容但要注意javax到jakarta的包名变化涉及定时任务里注入Service的代码要格外小心不过这属于编译期就能暴露的问题IDE一查就知道。只需要引入一个starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-quartz/artifactId /dependency这个starter会顺带把spring-context-support引进来Quartz调度器和Spring的Bean管理就能打通了。这里有个前提要记住spring-boot-starter-quartz是Spring Boot提供的统一封装它把Quartz的调度器工厂、任务类的实例化方式都接管了所以理论上我们自己写SchedulerFactoryBean的代码可以省略直接用自动配置就行。2.2 写一个Job的标准姿势Quartz里的任务核心是一个接口org.quartz.Job只有一个方法execute(JobExecutionContext context)。Spring为了让我们能在Job里注入自己的Service专门提供了QuartzJobBean这个抽象类它会通过Spring容器来创建Job实例而不是反射调用无参构造器。这一点非常关键稍后我们会专门说到。先写一个最简单的任务Component public class SampleJob extends QuartzJobBean { Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { // 模拟业务处理从上下文里取出传入的自定义参数 String jobName context.getJobDetail().getKey().getName(); JobDataMap dataMap context.getMergedJobDataMap(); System.out.println(Job [ jobName ] 开始执行参数: dataMap.getString(batchNo)); } }注意QuartzJobBean底层还是通过execute方法调用executeInternal这是框架替我们做好的转换。然后定义JobDetail和TriggerConfiguration public class QuartzConfig { Bean public JobDetail sampleJobDetail() { JobDataMap jobDataMap new JobDataMap(); jobDataMap.put(batchNo, 20250309-001); return JobBuilder.newJob(SampleJob.class) .withIdentity(sampleJob) .setJobData(jobDataMap) .storeDurably(true) .build(); } Bean public Trigger sampleJobTrigger() { return TriggerBuilder.newTrigger() .forJob(sampleJob) .withIdentity(sampleTrigger) .withSchedule(CronScheduleBuilder.cronSchedule(0 0/1 * * * ?)) .build(); } }关键点有几个withIdentity给任务和触发器都起了名字这个名字在Quartz内部是主键持久化后存库集群时争抢也靠它区分。storeDurably(true)把没有关联Trigger的JobDetail保存下来这为后面的动态新增任务打基础。Cron表达式用的是Quartz七段式最后多了一个?比Spring的Scheduled六段式多了一位秒。网上很多新手把CRON直接复制过来、最后一位没有?运行时倒是不会报错但规则会跟你想要的不一致。写完这两个BeanSpring Boot自动配置的SchedulerFactoryBean会自动把它们组装进Scheduler。任务就会按每分钟一次的节奏跑起来。这个时候我们已经完成了最基础的工作——一个能跑的Quartz任务。2.3 使用Component和Bean各自会发生什么这段有必要单独说因为这是整合时第一个容易迷路的点。如果你把SampleJob用Component标注同时又在Configuration里通过new SampleJob的方式构造JobDetail那么Quartz在执行的时候会向Spring询问SampleJob的Bean并优先使用Spring容器中的实例。这会和我们手上这个new出来的JobDetail产生一个让人困惑的差异JobDetail里引用的Job类名一致但实例来源不同。具体来说spring-boot-starter-quartz默认会通过Spring容器来解析Job实例。最终效果即便你把SampleJob声明为Component触发器激活的那一瞬间Spring依然会用ApplicationContext.getBean(SampleJob.class)去获取一个Bean来执行executeInternal。如果类路径上存在多个候选Bean可能启动就直接报错。所以实际项目中我的习惯是像SampleJob这种会被多个JobDetail引用的任务直接不标Component通过JobBuilder.newJob(xxxJob.class)传入类型交给Quartz维护一份自己的实例。需要注入依赖时在Job里直接Autowired即可因为Spring还是会帮我们做依赖处理。至于单例还是多例的问题Quartz默认每个JobDetail在每次执行时都会创建一个新的Job实例相当于prototype。不过这个默认行为也直接引出了下一个坑**无状态Job不带锁同一时刻如果上一次还没跑完下一次触发又到了会并发执行。**业务上如果不想重复处理数据务必在Job类上标注DisallowConcurrentExecution并且考虑使用带状态的持久化存储配合锁。3. 生产级关键一步JDBC持久化与重复触发问题3.1 先搞清Quartz的RAM和JDBC两种存储默认情况下Quartz的任务明细保存在内存里叫RAMJobStore。重启后任务配置全部丢失触发记录也没了。生产上绝对不能这样。Quartz自带JDBC JobStore把JobDetail、Trigger、Calendar、执行日志全部落到数据库表里。一旦持久化之后应用重启或者某台机器挂了调度信息依然存在集群中存活的节点可以继续接管。要把JDBC存储打开需要两样东西对应的数据库表和一份quartz.properties配置。建表脚本Quartz官方自带在quartz jar包里能翻到路径大概是org/quartz/impl/jdbcjobstore/里面有各种数据库方言的建表SQL比如tables_mysql_innodb.sql。注意不同Quartz版本脚本略有差异一定要解压当前引用的jar包去取对应脚本不要从网上随便复制一个旧版本的否则可能出现字段缺失、创建索引失败。把Quartz十张核心表的概念先讲清楚这对后面排查集群问题帮助特别大QRTZ_JOB_DETAILSJobDetail明细存放任务类型、描述和JobDataMap序列化结果。QRTZ_TRIGGERS触发器主表记录触发器类型、前后屏蔽时间、状态、关联的JobDetail。QRTZ_CRON_TRIGGERSCron触发器专用表存CRON表达式和时区。QRTZ_SIMPLE_TRIGGERSSimple触发器专用表存重复次数、间隔。QRTZ_BLOB_TRIGGERS触发器Blob类型存自定义触发器的二进制数据。QRTZ_FIRED_TRIGGERS正在执行的触发器记录执行完清除。QRTZ_SCHEDULER_STATE集群节点心跳状态表配合SCHED_NAME实现集群感知。QRTZ_LOCKS行级锁表使用数据库锁实现分布式锁。QRTZ_PAUSED_TRIGGER_GRPS暂停的触发器组。QRTZ_CALENDARS日历用于排除非工作日等场景。这些表本质上就是Quartz运行时的完整状态集。搞懂它们之后我们就知道数据库里哪些字段是可以在线修改、从而影响调度的。3.2 quartz.properties配置实操在src/main/resources下新增一个quartz.propertiesorg.quartz.scheduler.instanceName MyScheduler 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 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.misfireThreshold 120000 org.quartz.jobStore.isClustered true org.quartz.jobStore.clusterCheckinInterval 15000 org.quartz.jobStore.txIsolationLevelSerializable true org.quartz.dataSource.myQuartzDS.driver com.mysql.cj.jdbc.Driver org.quartz.dataSource.myQuartzDS.URL jdbc:mysql://127.0.0.1:3306/quartz?useSSLfalsecharacterEncodingutf8 org.quartz.dataSource.myQuartzDS.user root org.quartz.dataSource.myQuartzDS.password xxxxxx org.quartz.dataSource.myQuartzDS.maxConnections 10注意Quartz自己维护了一套独立数据源配置和Spring的DataSource互不干扰。至于是否要和业务库共用数据源我的建议是单独建一个quartz库或独立schema避免业务锁和Quartz锁互相影响。比如QRTZ_LOCKS表的行锁粒度很粗同一时刻一个节点只能跑有限的调度操作如果混在业务库高峰期可能出现莫名的锁等待。配置项里最容易被忽略的是misfireThreshold也就是“触发迟到多久算misfire触发丢失”。默认60000毫秒。如果任务很短又很多建议调大一些到两分钟。否则应用GC停顿或者某台机器暂时无响应时恢复后Quartz发现之前有大量trigger“错过”触发时间会按照MISFIRE策略猛补跑一次甚至几次一点都不符合预期。3.3 持久化后我踩过的“任务重复执行”的坑这个踩坑经历值得记录下来。某次我给一个内部数据同步任务升级到JDBC存储顺手把并发限制加上了DisallowConcurrentExecution本以为万事大吉。结果上线第二天凌晨发现同一批数据被两个实例同时处理了。看日志发现两个Pod都执行了而且都认为自己拿到了锁。原因要拆成两层第一DisallowConcurrentExecution的语义是通过JobDetail维度的锁来避免同一个JobDetail在同一时刻被并发执行。但是集群环境下如果仅仅用了RAM存储或者JDBC存储配置里没有开启isClusteredtrue那么两个节点各自持有调度器彼此感知不到对方这个注解的锁只在单进程内有效跨进程就失效了。第二我当时明明配了isClusteredtrue为什么还重复查下去发现是数据源指向了不同schema。我的Quartz数据源配置里URL写的是127.0.0.1:3306/quartz而另一个环境的配置是node1的本地库。两个节点的Scheduler实际上各自连着独立的quartz库互相之间完全不可见。它们都认为自己是唯一的调度器。确认原因后就好办了把两个节点指向同一个数据库连接再确认每台机器启动时日志中出现集群模式开启、能检测到其他节点的信息。另外集群模式下实例ID设置为AUTOQuartz会通过数据库生成唯一实例ID配合QRTZ_SCHEDULER_STATE表做心跳检测。一个节点挂了之后集群的故障转移默认也要等一个检查周期所以clusterCheckinInterval不能设得太小否则网络抖动会导致误杀设太大则故障接管慢。15秒这个值在绝大部分内网环境是合理的。后来我把调度逻辑改成这样对于“必须只执行一次”的任务在Job里用分布式锁再做一次业务层的幂等。**Quartz只能保证调度不重复见过不能保证你的处理逻辑天然幂等。**把调度层的保证和业务层的幂等分开对待才是正确姿势。3.4 misfire策略选不对错过的时间也会补Quartz的Trigger都支持misfire策略尤其CronTrigger。简单说就是如果到了触发时间可用线程不够或者调度器刚好宕机导致没触发成功恢复后会按策略决定是否补偿。默认的策略是FireOnceNow——立即补一次。这个策略对很多场景其实很危险比如一个任务每小时跑一次数据汇总凌晨宕机两小时恢复后补跑一次是合理的但如果任务是每30秒刷新一次缓存补跑意义不大反而打乱节奏。我的习惯做法只希望最新的下一次触发执行、不要补的用withMisfireHandlingInstructionDoNothing()。希望错过的周期不补、但从现在开始继续按原规则走的是ignoreMisfirePolicy。希望漏一次补一次的用withMisfireHandlingInstructionFireAndProceed()。具体在TriggerBuilder里就是这样写CronScheduleBuilder cronSchedule CronScheduleBuilder.cronSchedule(0 0 2 * * ?) .withMisfireHandlingInstructionDoNothing();注意trigger在创建时就带上了misfire策略后续如果通过rescheduleJob重新调度还要重新指定。这个坑我在代码里吃过亏程序启动时初始化好的trigger配置好了misfire策略但后台上运营人员改CRON的时候我用rescheduleJob去覆盖结果没显式设置misfire策略旧策略直接丢掉了恢复默认的FireOnceNow。某次凌晨数据库故障恢复后所有任务全量补跑了一遍把下游接口打爆了。现在我的写法是每次reschedule时都明确带上策略CronTrigger newTrigger TriggerBuilder.newTrigger() .withIdentity(oldTrigger.getKey()) .withSchedule(CronScheduleBuilder.cronSchedule(0 0 3 * * ?) .withMisfireHandlingInstructionDoNothing()) .build(); scheduler.rescheduleJob(oldTrigger.getKey(), newTrigger);4. 动态调度接口页面增删改定时任务的全套实现4.1 为什么必须有动态调度回到开头那个运营需求——“运营人员要在后台新建定时任务”。这意味着JobDetail、Trigger都不能写死在Configuration里而是运行时从数据库读取任务配置动态创建。Quartz的Scheduler结构天然满足这一点只是我们要自己把“业务配置表”和“Quartz任务”对应起来。我采用的方案是业务侧单独建一张sch_job_config表存任务名、CRON表达式、任务回调类型、参数JSON、状态等。后台页面操作这张配置表同时调用调度服务把变更同步到Quartz。这样配置有源可查和Quartz的执行状态分离审计日志也方便。4.2 动态新增任务的完整代码调度服务核心方法大概长这样省略了部分校验和日志Service public class QuartzManagerService { private final Scheduler scheduler; public QuartzManagerService(Scheduler scheduler) { this.scheduler scheduler; } public void addJob(JobConfigDTO config) throws Exception { // 1. 校验任务名是否已存在 if (scheduler.checkExists(new JobKey(config.getJobName(), config.getJobGroup()))) { throw new BizException(任务已存在: config.getJobName()); } // 2. 根据回调类型选择执行器 Class? extends Job jobClass chooseJobClass(config.getHandlerType()); // 3. 构造JobDetail附带业务参数 JobDataMap dataMap new JobDataMap(); dataMap.put(handlerType, config.getHandlerType()); dataMap.put(params, config.getParamsJson()); dataMap.put(timeout, config.getTimeoutSeconds()); JobDetail jobDetail JobBuilder.newJob(jobClass) .withIdentity(config.getJobName(), config.getJobGroup()) .usingJobData(dataMap) .storeDurably(true) .requestRecovery(true) .build(); // 4. 构造Cron触发器 CronTrigger trigger TriggerBuilder.newTrigger() .withIdentity(config.getJobName() _trigger, config.getJobGroup()) .withSchedule(CronScheduleBuilder.cronSchedule(config.getCronExpression()) .withMisfireHandlingInstructionDoNothing()) .build(); // 5. 注册到调度器 scheduler.scheduleJob(jobDetail, trigger); // 6. 持久化业务配置 saveConfig(config); } }几个值得说的细节requestRecovery(true)持久化模式下如果任务正在执行而调度器挂了Quartz会在恢复后重新调度这个Job执行。否则任务执行一半丢失可能没有任何补偿。任务组group我一般会用业务线作为group比如daily、report、scheduler。组是Quartz天生的任务分类维度后面批量暂停、批量删除都可以按组操作。任务名和触发器名要一一对应且唯一方便运维时通过任务名直接定位trigger。如果混乱后续维护就是灾难。判断任务是否已存在用scheduler.checkExists(new JobKey(...))这个方法在JDBC存储模式下会查库注意不要在高频循环里调用。4.3 更新、暂停、恢复、删除的完整实现动态调度要应对的完整场景远不止新增。我维护了下面这几个方法的实现每个都踩过相应的坑暂停任务scheduler.pauseJob(new JobKey(jobName, jobGroup));恢复任务scheduler.resumeJob(new JobKey(jobName, jobGroup));删除任务scheduler.deleteJob(new JobKey(jobName, jobGroup));注意deleteJob不仅会删除JobDetail还会移除所有关联的Trigger。如果只想改触发规则而不动任务本身不要用deleteJob要使用rescheduleJob。更新CRON则要先拿到旧触发器public void rescheduleJob(String jobName, String jobGroup, String newCron) throws Exception { JobKey jobKey new JobKey(jobName, jobGroup); if (!scheduler.checkExists(jobKey)) { throw new BizException(任务不存在); } // 获取已有触发器这里有个前提一个JobDetail通常绑定一个Trigger // 如果绑了多个需要遍历 triggersOfJob List? extends Trigger triggers scheduler.getTriggersOfJob(jobKey); if (triggers.isEmpty()) { // 说明是仅有JobDetail没有触发器的持久任务可以再生成一个trigger CronTrigger newTrigger TriggerBuilder.newTrigger() .forJob(jobKey) .withIdentity(jobName _trigger, jobGroup) .withSchedule(CronScheduleBuilder.cronSchedule(newCron) .withMisfireHandlingInstructionDoNothing()) .build(); scheduler.scheduleJob(newTrigger); return; } TriggerKey oldTriggerKey triggers.get(0).getKey(); CronTrigger newTrigger TriggerBuilder.newTrigger() .withIdentity(oldTriggerKey) .withSchedule(CronScheduleBuilder.cronSchedule(newCron) .withMisfireHandlingInstructionDoNothing()) .build(); scheduler.rescheduleJob(oldTriggerKey, newTrigger); }这里有两个很容易忽略的坑第一个修改CRON后旧trigger的misfire策略丢失。我在3.4里已经说过了这里再强调一次。reschedule时如果builder里没写misfire策略它默认变回FireOnceNow这会让错过时间的trigger疯狂补跑。第二个任务更新期间如果业务方同时操作了任务可能造成状态不一致。我的解决方案是用配置表的乐观锁版本号去控制更新在事务提交之后再调用调度器真正执行reschedule。事务不要和Quartz调度操作混在同一个事务里因为Quartz自身的数据库写入在它自己的事务里一旦调度器执行成功而业务事务回滚也会出现配置与调度不一致的情况。我的标准动作是业务配置表先做UPDATE在自己的事务里提交成功后再调scheduler方法。这样即使调度操作失败下次任务周期内也可以做一个“配置与调度对账”任务弥补。4.4 使用JobDataMap传参的正确打开方式JobDataMap本质是一个和JobDetail/Trigger关联的Map可以被持久化。每次触发的时候executeInternal里通过context.getMergedJobDataMap()拿到的数据是JobDetail的JobDataMap和Trigger的JobDataMap合并后的结果。两个地方都设置了同名key时触发器的值会覆盖JobDetail的值。这里要强调一个使用规则JobDataMap虽然能存任意可序列化对象但强烈建议只存“类型标识、配置键、ID”这类轻量参数。比如传一个数据库主键ID任务执行时再根据ID去查全量数据。把重量级对象丢进去序列化成本高集群模式下还可能因为对象内部状态变化导致数据不一致。还有JobDataMap在任务执行时是“快照”使用的。运行时修改context.getJobDetail().getJobDataMap()的数据不会影响已经生成的trigger记录但会影响下一次执行的取值。所以如果想做“本次执行成功后才把某个参数标记为已完成”不要在内存里改应该更新业务表下次任务启动时自己查。5. 集群部署时必须处理的另外三件事5.1 每个任务在业务层尽量做幂等这个观点我在前面反复提过但值得单独展开。Quartz集群保证的是“调度层面的不重复”——同一个trigger在同一时刻只会分配给一个节点的线程池执行。但业务处理本身的幂等Quartz不负责。比如一个任务要调第三方接口拉数据网络超时后重试或者两个任务逻辑上处理同一批数据先后都触发了。幂等最简单有效的实现是给业务表加一个“批号/业务ID”唯一约束执行时insert或update时捕获冲突直接忽略。举例我在做对外数据同步任务时数据源表会生成batch_number目标表里对batch_number建唯一索引。任务里带上这个batch执行时先尝试insert重复键就跳过。这样即使Quartz或者运维手动补跑也不会造出脏数据。5.2 有状态Job如何配合PersistJobDataAfterExecution默认情况下Job实例每次执行是全新对象execute方法里对JobDataMap做的修改不会自动保存。如果希望本次执行修改的参数比如重试次数、游标位置能被下一次执行看到需要在Job类上加上PersistJobDataAfterExecution注解告诉Quartz在该Job执行完后将JobDataMap写回JobDetail。同时配合DisallowConcurrentExecution使用官方文档也比较推荐。原因很直白如果你允许并发执行同一个JobDetail一个实例写回JobDataMap另一个实例也写回就互相覆盖了。实际工作中我见过不少人把这两个注解当成“任务必须串行”的银弹只挂注解却没有真正搞懂它们的语义。它们在单进程内效果确实明显但集群下仍需配合isClustered配置并且业务幂等不能省。5.3 注意Quartz自身的数据库锁和连接池JDBC JobStore的分布式锁实现是靠QRTZ_LOCKS表行锁。Quartz会先通过select for update锁住某一行再执行调度状态变更这个锁的粒度是调度器级别的。这意味着集群节点越多、调度频率越高QRTZ_LOCKS表的锁竞争可能越严重。所以我给Quartz单独配了连接池避免和业务DB连接互相饿死。另外Quartz的秒级高频任务比如每5秒触发一次如果量大数据库IO会非常夸张。我们线上任务CRON基本都是分钟级以上秒级任务不是Quartz擅长的主场。真要秒级频率建议考虑专业消息队列或者专门的流处理框架别硬塞进Quartz。6. 可直接抄走的落地配置清单这节给一份可以直接“抄作业”的落地清单按顺序执行基本不会出问题建库单独建quartz库执行当前Quartz版本对应的tables_mysql_innodb.sql。依赖引入spring-boot-starter-quartz。资源把quartz.properties放在classpath下。业务任务类写一个继承QuartzJobBean的类业务要求串行时标注DisallowConcurrentExecution。启动入口任务用Configuration定义几个固定的JobDetail和Trigger用于系统预置任务。动态任务接口新增一个Facade服务把addJob、rescheduleJob、pauseJob、resumeJob、deleteJob这些调度操作封装好。幂等兜底Job执行方法里用唯一约束或分布式锁做业务幂等不指望Quartz单独保证。监控定时扫描QRTZ_FIRED_TRIGGERS和业务配置表找出“配置了却没被调度”的任务以及“执行时间过长”的任务及时报警。这一段不是总结而是实实在在的交付清单。我在接手过的若干个定时任务改造项目里都是按这个清单往下推进的基本不踩坑。具体到每个公司的基础设施不同数据库用Oracle的、PostgreSQL的或者用Spring的Hikari连接池接Quartz的只需要在quartz.properties里改对应驱动类和方言就行。还有一个小提醒别自作聪明给Quartz的库表加索引之外的扩展字段。Quartz的ORM逻辑很死板表结构一改动它的SQL可能直接报错或者出现隐藏的并发问题。业务相关的扩展字段放自己的配置表通过JobDataMap传ID关联即可。这是我最初踩过的一个坑给QRTZ_JOB_DETAILS加过一列后来升级Quartz版本时建表脚本跑不过去折腾了好久才撤销。7. 我自己用下来的几点体会Quartz这个东西一开始接触会觉得API有点绕JobDetail、Trigger、Scheduler三层结构还有注解、监听器、misfire策略一堆概念。但只要完整跑通一次集成过程会发现它的设计是真的经得起推敲。Job解决“做什么”Trigger解决“什么时候做”Scheduler解决“怎么做并管理状态”。两层分离意味着任务和触发规则可以单独替换这在动态需求面前太重要了。我个人的体会是别贪图快直接照抄配置就完事。其实先花半小时把QRTZ表结构和那几条SQL看一遍后面排查问题会快很多。Quartz遇到诡异问题时最好的调试工具就是查数据库里各张表当前存了什么状态。QRTZ_TRIGGERS表里TRIGGER_STATE从WAITING变成ACQUIRED再变成EXECUTING这个状态流转看懂之后任何任务“该跑没跑”“多台都跑”“没按点跑”的问题都能在库里明确对应出来。这篇把整合到集群部署的思路和细节都过了一遍。最后再分享一个小技巧如果你在测试环境经常反复启动应用记得把org.quartz.scheduler.skipUpdateChecktrue加上否则Quartz每次启动都会往Quartz官网发一次版本检查请求虽然不影响功能但会让启动日志多出一些无意义的网络超时提示看着很碍眼。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。