Spring Boot中Quartz编程式调度任务触发时间并发多次执行问题
咱们来一步步排查你遇到的Quartz任务在触发时间点并发执行多次的问题,结合你给出的代码片段,主要有这几个可能的原因和对应的解决办法:
1. 重复获取Scheduler实例导致旧实例未被关闭
你在初始化调度器时调用了StdSchedulerFactory.getDefaultScheduler(),但在@PreDestroy的 shutdown 方法里又重新获取了一个新的Scheduler实例来关闭——这就会导致最初启动的那个Scheduler根本没被正确关闭!如果应用重启或者上下文刷新,旧的调度器还在后台偷偷运行,新的又启动,自然会出现同一个任务被多个调度器同时触发的情况。
解决办法:把Scheduler实例作为类的成员变量保存下来,复用同一个实例进行初始化和关闭:
private Scheduler scheduler; // 初始化调度器(用@PostConstruct确保在Bean初始化后执行) @PostConstruct public void initScheduler() throws SchedulerException { scheduler = StdSchedulerFactory.getDefaultScheduler(); // 创建Job和Trigger JobDetail jobDetail = newJob(ExPJob.class).build(); CronScheduleBuilder cronSchedule = cronSchedule(mycronExpression) .withMisfireHandlingInstructionDoNothing(); Trigger trigger = newTrigger() .withSchedule(cronSchedule) .build(); scheduler.scheduleJob(jobDetail, trigger); scheduler.start(); } @PreDestroy public void shutdownSch() throws SchedulerException { // 检查实例是否存在且未关闭,再执行关闭 if (scheduler != null && !scheduler.isShutdown()) { scheduler.shutdown(true); // true表示等待正在执行的任务完成后再关闭 } }
2. 未禁止Job的并发执行
Quartz默认允许同一个Job的多个实例并发执行,如果你的任务执行时间超过了Cron表达式的间隔,或者因为misfire触发了补跑,就会出现多个任务实例同时运行的情况。你虽然设置了withMisfireHandlingInstructionDoNothing()跳过错过的触发,但如果任务本身执行耗时较长,还是会出现并发问题。
解决办法:在你的ExPJob类上添加@DisallowConcurrentExecution注解,禁止同一个JobDetail的多个实例并发执行:
import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; @DisallowConcurrentExecution public class ExPJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { // 你的任务业务逻辑 } }
3. Spring上下文重复加载导致调度器多次初始化
如果你的应用在启动或部署过程中Spring上下文被多次加载(比如开发环境用了热部署、容器配置问题等),会导致调度器被多次创建并启动,进而引发任务并发执行。
解决办法:用Spring的@Configuration配合@Bean来管理Scheduler的单例,确保调度器只会被初始化一次:
import org.quartz.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class QuartzConfig { @Bean public Scheduler scheduler() throws SchedulerException { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); JobDetail jobDetail = newJob(ExPJob.class).build(); CronScheduleBuilder cronSchedule = cronSchedule(mycronExpression) .withMisfireHandlingInstructionDoNothing(); Trigger trigger = newTrigger() .withSchedule(cronSchedule) .build(); scheduler.scheduleJob(jobDetail, trigger); scheduler.start(); return scheduler; } }
Spring会自动管理这个Scheduler Bean的生命周期,在上下文销毁时自动调用shutdown方法(Quartz的StdScheduler实现了DisposableBean接口),无需手动写@PreDestroy。
内容的提问来源于stack exchange,提问作者TriGun

