Quartz 定时任务到点不执行misfire 策略与并发锁复盘导读早上九点运营等着结算任务跑完结果任务没触发日志里一片安静。查了半天问题不在代码在 Quartz 的 misfire 策略和线程池配置。定时任务不执行十有八九不是 Cron 写错而是调度器自己吞了。Quartz 的 misfire错失触发机制一句话任务该跑的时候没跑调度器会在恢复后按策略补偿。策略选错轻则任务不跑重则补跑把线上数据搞乱。加上 Quartz 默认允许同一个 job 并发执行两个 trigger 实例一起跑更是事故温床。先看配置这两个参数定生死qkl-boot 的定时任务配置里overwriteExistingJobs和线程池大小是很多人忽略的地方# application.ymlspring:quartz:job-store-type:jdbc# 用 JDBC 存储支持集群overwrite-existing-jobs:true# 同名 job 已存在时覆盖防止重复注册properties:org.quartz.scheduler.instanceName:QklSchedulerorg.quartz.scheduler.instanceId:AUTOorg.quartz.threadPool.threadCount:8# 线程池大小坑在这org.quartz.threadPool.threadPriority:5org.quartz.jobStore.isClustered:true# 集群模式org.quartz.jobStore.clusterCheckinInterval:20000org.quartz.jobStore.misfireThreshold:60000# 超时 60s 算 misfirethreadCount如果设成 1所有任务排队跑一个任务卡住后面全堵——这是任务没执行的一个隐蔽原因。另一个是misfireThreshold任务被延迟超过这个阈值才记 misfire阈值设太小比如 3000ms网络抖动一下就把正常任务误判成 misfire触发补跑。Job 与 Trigger 的正确姿势Spring 里注册 Quartz Job 有两种路子继承QuartzJobBean或者直接声明 JobDetail Trigger Bean。qkl-boot 用的是后者把调度参数外置到配置后台可配ConfigurationpublicclassQuartzConfig{BeanpublicJobDetailsettlementJobDetail(){returnJobBuilder.newJob(SettlementJob.class).withIdentity(settlementJob).storeDurably()// 即使没有 trigger 也保留 job.requestRecovery()// 集群中执行时进程挂了别处恢复执行.build();}BeanpublicTriggersettlementTrigger(Qualifier(settlementJobDetail)JobDetaildetail){returnTriggerBuilder.newTrigger().forJob(detail).withIdentity(settlementTrigger).withSchedule(CronScheduleBuilder.cronSchedule(0 30 1 * * ?)// 每天 01:30 跑结算.withMisfireHandlingInstructionDoNothing())// 错过就不补.build();}}Misfire 策略三选一按业务性质定withMisfireHandlingInstructionDoNothing()错过就错过不补跑。适合今天的结算今天必须算昨天没算今天补没意义——补跑反而会把两天的数据算混。qkl-boot 的日结结算用的是这个。withMisfireHandlingInstructionFireAndProceed()立即补跑一次然后按原计划继续。适合日报生成这种补跑有意义的任务。withMisfireHandlingInstructionIgnoreMisfires()把错过的都补完。慎用错过 10 次补 10 次数据量大的任务能把自己跑挂。并发执行同一个 Job 会同时跑两个实例Quartz 默认不禁止同一个 JobDetail 的并发执行。调度器宕机重启、集群里两台机器同时调度都可能让同一个 job 的两个实例同时跑。对扣钱、发消息、生成结算单这类任务并发跑一遍就是灾难。两个解法方案 ADisallowConcurrentExecution单机/单调度器推荐DisallowConcurrentExecutionpublicclassSettlementJobextendsQuartzJobBean{OverrideprotectedvoidexecuteInternal(JobExecutionContextcontext){// 上一个实例没跑完这个实例不会启动doSettlement();}}方案 B分布式锁集群场景必上DisallowConcurrentExecution只在同一个调度器实例内生效多节点部署时各节点的 scheduler 是独立的注解管不住跨节点并发。集群里要靠 Redis 锁兜底DisallowConcurrentExecutionpublicclassSettlementJobextendsQuartzJobBean{AutowiredprivateStringRedisTemplateredis;OverrideprotectedvoidexecuteInternal(JobExecutionContextcontext){Stringkeylock:settlement:20261013;StringtokenUUID.randomUUID().toString();Booleanokredis.opsForValue().setIfAbsent(key,token,30,TimeUnit.MINUTES);if(Boolean.FALSE.equals(ok)){log.warn(结算任务已被其他节点执行跳过。node{},context.getFireInstanceId());return;}try{doSettlement();}finally{// 只释放自己拿到的锁防误删别人的锁Stringcurredis.opsForValue().get(key);if(token.equals(cur))redis.delete(key);}}}锁过期时间要大于任务最大执行时长——任务跑了 40 分钟锁 30 分钟就过期了另一个节点又能抢到锁等于没锁。加锁和解锁必须配套finally 里释放token 校验防误删。踩坑实录凌晨 1 点 30 的结算任务一连三天没跑现象运营连着三天早上查账发现昨天的结算单没生成后台任务列表显示上次执行时间还是三天前。任务没报错日志里连一条异常都没有。排查过程先看任务列表的 trigger 状态——Quartz 管理界面里 trigger 显示ERROR不是NORMAL。再查日志找到关键行2026-10-10 01:30:00.001 ERROR o.s.s.quartz.LocalDataSourceJobStore - Couldnt fire trigger ... job will be executed at next interval Caused by: org.quartz.SchedulerException: Job threw an unhandled exception: java.sql.SQLException: Lock wait timeout exceeded; try restarting transaction也就是说任务其实触发了但执行中报锁等待超时Quartz 认为它执行失败把 trigger 状态置成ERROR。而misfireThreshold60000且策略是 DoNothing——misfire 被判定后直接放弃后面的周期也不再自动恢复任务就死在 ERROR 状态里。定位思路任务本身没问题是执行环境和 SQL 冲突。结算任务深夜跑和大批量的对账 SQL 抢 InnoDB 行锁锁等待超过innodb_lock_wait_timeout默认 50s抛异常。Quartz 捕获异常后不重试trigger 变 ERROR 卡死。根因是任务执行失败 misfire 策略 DoNothing ERROR 状态不自愈三件事叠在一起。最终解决三步改完第二天任务恢复正常SQL 层面结算查询大表时避免长事务分批取数每批 2000 条把单事务时长压到 10 秒内调度层面给关键任务加Redis 分布式锁上面的方案 B并注册JobListener监听jobWasExecuted捕获异常时手动把 trigger 状态复位// 复位任务失败自动把 trigger 恢复为可调度scheduler.resumeTrigger(TriggerKey.triggerKey(settlementTrigger));监控层面给任务执行加成功/失败埋点失败告警到企业微信——不能等运营早上来问。可以直接抄的结论misfire 策略按业务选日结类用 DoNothing日报类用 FireAndProceedIgnoreMisfires 慎用threadCount别设 1misfireThreshold别小于 10s关键任务加DisallowConcurrentExecution单机或 Redis 锁集群锁要带 token 防误删、过期时间大于任务最大时长任务失败要监听jobWasExecuted并复位 trigger否则 ERROR 状态会死到天荒地老大表操作避免长事务分批取数把锁等待风险压下去。定时任务不执行十次里八次不是 Cron 写错是调度器配置和任务失败处理的问题。把 misfire、并发、失败恢复三条线理清楚定时任务才能真正定时。项目源码https://gitee.com/gzqkl/qkl-boot