Quartz触发器过期时间配置异常:重启后执行已过期触发器
我实现了Quartz的JobSchedulerService如下:
@Slf4j @Service @RequiredArgsConstructor public class QuartzServiceImpl implements JobSchedulerService { private final SchedulerFactoryBean schedulerFactoryBean; @Override public void scheduleJob(LocalDateTime date, Class jobClass, boolean repeatUntilManuallyStopped, Map<String, Object> jobDataMap) { String expirationDate = date.toString(); String name = jobClass.getName() + "_"; JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(name + expirationDate) .storeDurably().build(); if(jobDataMap != null){ jobDetail.getJobDataMap().putAll(jobDataMap); } jobDetail.getJobDataMap().put("expirationDate", expirationDate); ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault()); SimpleTrigger trigger = TriggerBuilder.newTrigger().withIdentity(name + expirationDate) .startAt(Date.from(zonedDateTime.toInstant())) .endAt(Date.from(zonedDateTime.plusMinutes(2).toInstant())) .withSchedule(repeatUntilManuallyStopped ? SimpleScheduleBuilder.repeatMinutelyForever().withMisfireHandlingInstructionFireNow() : SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow()) .build(); schedule(jobDetail, trigger, false); } @Override public void scheduleJob(LocalDateTime date, int intervalInMinutes, int count, Class jobClass, Map<String, Object> jobDataMap) { String expirationDate = date.toString(); String name = jobClass.getName() + "_"; JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(name + expirationDate) .storeDurably().build(); jobDetail.getJobDataMap().putAll(jobDataMap); jobDetail.getJobDataMap().put("expirationDate", expirationDate); LocalDateTime currentTime = LocalDateTime.now(); LocalDateTime startDateTime = date; int decrement = count; while (decrement != 0) { startDateTime = startDateTime.minusMinutes(intervalInMinutes); decrement --; if(currentTime.compareTo(startDateTime) >= 0) { startDateTime = currentTime; break; } } SimpleTrigger trigger = TriggerBuilder.newTrigger().withIdentity(name + expirationDate) .startAt(Date.from(startDateTime.atZone(ZoneId.systemDefault()).toInstant())) .withSchedule(SimpleScheduleBuilder.repeatMinutelyForTotalCount(count - decrement, intervalInMinutes) .withMisfireHandlingInstructionFireNow()) .build(); schedule(jobDetail, trigger, false); } public Date schedule(JobDetail jobDetail, SimpleTrigger trigger, boolean isUnique) { Scheduler scheduler = schedulerFactoryBean.getScheduler(); try { boolean alreadyExists = scheduler.checkExists(jobDetail.getKey()); if(isUnique && alreadyExists) { throw new ApiException(ApiError.RESOURCE_EXISTS); } else if(!alreadyExists){ Date date = scheduler.scheduleJob(jobDetail, trigger); scheduler.start(); return date; } else { return trigger.getStartTime(); } } catch (ObjectAlreadyExistsException e) { if(isUnique) { ExceptionTracker.captureException(e); log.error("Could not schedule job with key {} and description {}. Job already exists", jobDetail.getKey().getName(), jobDetail.getDescription()); throw new ApiException(ApiError.INTERNAL_SERVER_ERROR); } else { return trigger.getStartTime(); } } catch (Exception e){ ExceptionTracker.captureException(e); log.error("Could not schedule job with key {} and description {}. Localized message => {}", jobDetail.getKey().getName(), jobDetail.getDescription(), e.getLocalizedMessage()); throw new ApiException(ApiError.INTERNAL_SERVER_ERROR); } } }
我给触发器设置了启动后2分钟过期:
.endAt(Date.from(zonedDateTime.plusMinutes(2).toInstant()))
但创建任务和触发器后立即关闭应用,重启后发现即便qrtz_triggers表中的end_time已经过期,系统还是会执行这个过期的触发器。
我的Quartz配置文件:
org.quartz.scheduler.instanceName=PostcreditService org.quartz.scheduler.instanceId=AUTO org.quartz.threadPool.threadCount=5 org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate org.quartz.jobStore.useProperties=true org.quartz.jobStore.misfireThreshold=60000 org.quartz.jobStore.tablePrefix=postcredit.qrtz_ org.quartz.jobStore.isClustered=true org.quartz.plugin.shutdownHook.class=org.quartz.plugins.management.ShutdownHookPlugin org.quartz.plugin.shutdownHook.cleanShutdown=TRUE
Quartz配置类:
@Configuration public class QuartzConfig { @Autowired private ApplicationContext applicationContext; @Autowired private DataSource dataSource; @Bean public JobFactory jobFactory() { AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory(); jobFactory.setApplicationContext(applicationContext); return jobFactory; } @Bean public SchedulerFactoryBean schedulerFactoryBean() throws IOException { SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean(); schedulerFactory.setQuartzProperties(quartzProperties()); schedulerFactory.setWaitForJobsToCompleteOnShutdown(true); schedulerFactory.setAutoStartup(true); schedulerFactory.setJobFactory(jobFactory()); schedulerFactory.setDataSource(dataSource); return schedulerFactory; } public Properties quartzProperties() throws IOException { PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties")); propertiesFactoryBean.afterPropertiesSet(); return propertiesFactoryBean.getObject(); } }
恳请协助解决该问题!
问题根源
你设置的withMisfireHandlingInstructionFireNow()是核心问题:当触发器因应用关闭错过触发时间时,这个策略会强制立即执行一次,完全忽略触发器是否已经过期。加上集群模式下,Quartz重启时会扫描所有未标记完成的触发器,只要触发时间在misfireThreshold阈值内,就会触发这个强制执行逻辑。
解决步骤
替换失火处理策略
对有过期时间的触发器,改用withMisfireHandlingInstructionNextWithExistingCount(),这个策略会跳过所有错过的触发,直接从下一次有效时间开始执行;如果已经过了end_time,就不会再执行。修改第一个scheduleJob方法的触发器构建代码:SimpleTrigger trigger = TriggerBuilder.newTrigger().withIdentity(name + expirationDate) .startAt(Date.from(zonedDateTime.toInstant())) .endAt(Date.from(zonedDateTime.plusMinutes(2).toInstant())) .withSchedule(repeatUntilManuallyStopped ? SimpleScheduleBuilder.repeatMinutelyForever() .withMisfireHandlingInstructionNextWithExistingCount() // 替换失火策略 : SimpleScheduleBuilder.simpleSchedule() .withMisfireHandlingInstructionNextWithExistingCount()) .build();添加过期触发器主动清理逻辑
在schedule方法中,新增触发器存在性校验时的过期判断,直接删除已过期的触发器和任务,避免无效执行:public Date schedule(JobDetail jobDetail, SimpleTrigger trigger, boolean isUnique) { Scheduler scheduler = schedulerFactoryBean.getScheduler(); try { boolean alreadyExists = scheduler.checkExists(jobDetail.getKey()); // 新增:校验已存在的触发器是否过期 if(alreadyExists){ Trigger existingTrigger = scheduler.getTrigger(trigger.getKey()); if(existingTrigger != null && existingTrigger.getEndTime().before(new Date())){ // 删除过期的触发器和任务 scheduler.unscheduleJob(existingTrigger.getKey()); scheduler.deleteJob(jobDetail.getKey()); alreadyExists = false; } } if(isUnique && alreadyExists) { throw new ApiException(ApiError.RESOURCE_EXISTS); } else if(!alreadyExists){ Date date = scheduler.scheduleJob(jobDetail, trigger); scheduler.start(); return date; } else { return trigger.getStartTime(); } } catch (ObjectAlreadyExistsException e) { if(isUnique) { ExceptionTracker.captureException(e); log.error("Could not schedule job with key {} and description {}. Job already exists", jobDetail.getKey().getName(), jobDetail.getDescription()); throw new ApiException(ApiError.INTERNAL_SERVER_ERROR); } else { return trigger.getStartTime(); } } catch (Exception e){ ExceptionTracker.captureException(e); log.error("Could not schedule job with key {} and description {}. Localized message => {}", jobDetail.getKey().getName(), jobDetail.getDescription(), e.getLocalizedMessage()); throw new ApiException(ApiError.INTERNAL_SERVER_ERROR); } }可选:调整集群配置
如果你的业务场景不需要分布式集群,可以把配置文件中的org.quartz.jobStore.isClustered改为false,减少分布式场景下的触发器竞争和校验逻辑,进一步降低无效执行的概率。
验证方法
- 创建触发器后,等待2分钟让其过期,然后关闭应用再重启
- 查看qrtz_triggers表中该触发器的状态,同时检查应用日志,确认不会再执行过期的触发器
内容的提问来源于stack exchange,提问作者Inspector Gadget

