You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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阈值内,就会触发这个强制执行逻辑。

解决步骤

  1. 替换失火处理策略
    对有过期时间的触发器,改用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();
    
  2. 添加过期触发器主动清理逻辑
    在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);
        }
    }
    
  3. 可选:调整集群配置
    如果你的业务场景不需要分布式集群,可以把配置文件中的org.quartz.jobStore.isClustered改为false,减少分布式场景下的触发器竞争和校验逻辑,进一步降低无效执行的概率。

验证方法

  • 创建触发器后,等待2分钟让其过期,然后关闭应用再重启
  • 查看qrtz_triggers表中该触发器的状态,同时检查应用日志,确认不会再执行过期的触发器

内容的提问来源于stack exchange,提问作者Inspector Gadget

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 15:30:58