Quartz1.5.2+Spring3.2.1重调度任务首次执行两次问题求助
我先帮你拆解问题根源,再给出适配1.5.2版本的针对性修复方案——毕竟你没法升级版本,咱们就盯着现有环境解决问题。
问题根源分析
你遇到的重复执行问题,主要来自两个方面:
Spring上下文重复加载导致Scheduler实例重复
看你的web.xml配置,ContextLoaderListener和DispatcherServlet都加载了相同的spring-context-*.xml配置文件。这会导致Spring初始化两个独立的上下文,Quartz的SchedulerFactoryBean被实例化两次——相当于有两个调度器在同时运行任务,重调度后自然会出现重复执行的情况。Quartz 1.5.2自身的rescheduleJob逻辑缺陷
在Quartz 1.x版本中,调用rescheduleJob复用原有触发器时,如果旧触发器的下一次触发时间已经临近或到达当前时间,Quartz内部可能会在移除旧触发器前,已经把触发事件放入执行队列;而新触发器添加后,又会生成一次触发事件,最终导致两次执行。即使你设置了concurrent=false,也只是防止任务并发执行,队列里的两次事件还是会依次执行。
修复方案
第一步:优先修复Spring上下文重复加载问题
修改web.xml中DispatcherServlet的配置,让它只加载Spring MVC专属的配置文件,不要和根上下文重复加载Quartz的配置:
<servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> <!-- 单独存放MVC相关配置,比如控制器、视图解析器等 --> </init-param> </servlet>
根上下文(由ContextLoaderListener加载)负责加载Quartz、业务层等核心配置,子上下文(DispatcherServlet加载)只负责MVC相关配置,这样就避免了Scheduler被实例化两次。
第二步:修改重调度逻辑,避免触发器触发重叠
如果你坚持复用原有触发器,可以在重调度时先暂停触发器,修改表达式后设置延迟启动时间,避免旧触发事件残留:
public void resetJob(String expression){ System.out.println("****************change to:\t" + expression); ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(context); Scheduler scheduler = (Scheduler) applicationContext.getBean("testScheduler"); try { CronTriggerBean trigger = (CronTriggerBean) scheduler.getTrigger("testJobTrigger", Scheduler.DEFAULT_GROUP); // 先暂停触发器,防止在修改过程中触发 scheduler.pauseTrigger("testJobTrigger", Scheduler.DEFAULT_GROUP); // 更新cron表达式 trigger.setCronExpression(expression); // 设置触发器的启动时间为当前时间1秒后,避免立即触发旧的时间点 trigger.setStartTime(new Date(System.currentTimeMillis() + 1000)); // 重新调度触发器 scheduler.rescheduleJob("testJobTrigger", Scheduler.DEFAULT_GROUP, trigger); // 恢复触发器运行 scheduler.resumeTrigger("testJobTrigger", Scheduler.DEFAULT_GROUP); } catch (SchedulerException e) { e.printStackTrace(); } catch (ParseException e) { e.printStackTrace(); } }
如果上面的方法还是有问题,你可以改为先删除旧触发器,再创建新的触发器添加进去(你提到这种方式能解决问题,本质是彻底清除了旧触发器的残留事件):
public void resetJob(String expression){ System.out.println("****************change to:\t" + expression); ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(context); Scheduler scheduler = (Scheduler) applicationContext.getBean("testScheduler"); try { // 获取旧触发器和对应的JobDetail CronTriggerBean oldTrigger = (CronTriggerBean) scheduler.getTrigger("testJobTrigger", Scheduler.DEFAULT_GROUP); // 先暂停并移除旧触发器 scheduler.pauseTrigger("testJobTrigger", Scheduler.DEFAULT_GROUP); scheduler.unscheduleJob("testJobTrigger", Scheduler.DEFAULT_GROUP); // 创建新的触发器 CronTriggerBean newTrigger = new CronTriggerBean(); newTrigger.setName("testJobTrigger"); newTrigger.setGroup(Scheduler.DEFAULT_GROUP); newTrigger.setJobDetail(oldTrigger.getJobDetail()); newTrigger.setCronExpression(expression); // 同样设置延迟启动,避免立即触发 newTrigger.setStartTime(new Date(System.currentTimeMillis() + 1000)); // 添加新触发器并启动 scheduler.scheduleJob(newTrigger); scheduler.resumeTrigger("testJobTrigger", Scheduler.DEFAULT_GROUP); } catch (SchedulerException e) { e.printStackTrace(); } catch (ParseException e) { e.printStackTrace(); } }
补充说明
你提到Quartz 2.3.0复用触发器也有这个问题,其实是因为不同版本的Quartz在触发事件队列的处理逻辑上有差异,但2.x版本的优化更多,出现这个问题的概率更低。而1.5.2作为较老的版本,这个缺陷比较明显,所以需要通过手动控制触发器的暂停/延迟启动来规避。
内容的提问来源于stack exchange,提问作者flyingfox

