Spring Quartz集群环境下SimpleTrigger失败任务未执行问题
Spring Quartz集群SimpleTrigger故障转移失效问题排查
问题现象
基于Spring Quartz搭建JDBC持久化集群时,CronTrigger在节点宕机场景下可正常被其他节点接管执行,但SimpleTrigger类型任务在运行过程中节点宕机后,无法被存活节点接管,ClusterManager扫描故障实例时输出如下日志:
2022-06-22 13:37:52.659 INFO 23852 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore : ClusterManager: detected 2 failed or restarted instances. 2022-06-22 13:37:52.661 INFO 23852 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore : ClusterManager: Scanning for instance "MacBook-Pro.local1655885157031"'s failed in-progress jobs. 2022-06-22 13:37:52.677 INFO 23852 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore : ClusterManager: Scanning for instance "MacBook-Pro.local1655885169333"'s failed in-progress jobs. 2022-06-22 13:37:52.720 INFO 23852 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore : ClusterManager: ......Deleted 1 complete triggers(s). 2022-06-22 13:37:52.722 INFO 23852 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore : ClusterManager: ......Cleaned-up 1 other failed job(s).
复现步骤:
- 启动两个分别监听8080、8081端口的集群节点
- 通过8081端口节点调度一个SimpleTrigger类型任务
- 任务执行过程中强制关闭8081端口节点
相关代码与配置如下:
任务实现类:
public class ApiJob implements Job { final static Logger log = LoggerFactory.getLogger(ApiJob.class); @Override public void execute(JobExecutionContext context) { this.context=context; log.info("Job Execution Started"); JobDataMap map=context.getMergedJobDataMap(); ApiRequest request=new ApiRequest(map.getString("message")); try { Thread.sleep(2*60*1000); log.info("Job scheduled...{}",context.getJobDetail().getKey().getName()); } catch (InterruptedException e) { throw new RuntimeException(e); } } }
触发器构建代码:
Trigger trigger=TriggerBuilder.newTrigger().forJob(jobDetail) .withIdentity(jobDetail.getKey().getName(), "quartz-jobs-triggers") .withDescription("Random trigger") .startAt(Date.from(startTime.toInstant())) .withSchedule(SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow()) .build();
Quartz集群配置:
#Quartz Properties spring.quartz.job-store-type=jdbc spring.quartz.properties.org.quartz.threadPool.threadCount=5 spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO spring.quartz.properties.org.quartz.jobStore.isClustered = true spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval = 20000
根因分析
问题由两个核心逻辑认知偏差导致:
- 失火策略作用范围不匹配
配置的withMisfireHandlingInstructionFireNow()仅针对错过预定触发时间、尚未进入执行状态的失火触发器生效,对已经被节点拉取、处于执行中状态时节点宕机的任务完全不生效。从日志中Deleted 1 complete triggers(s).可以看出,故障扫描时集群直接把这个单次执行的SimpleTrigger标记为已完成删除,根本没有走到失火处理分支。 - 未开启任务恢复标记
Quartz默认不会对执行中宕机的任务做恢复重跑,判断是否需要恢复的核心开关是JobDetail的requestsRecovery属性,默认值为false。
另外CronTrigger能正常转移是因为CronTrigger属于周期性重复触发器,本身存在下一次触发时间,故障扫描时不会被直接删除,会重置为等待触发状态被存活节点后续调度;而当前构建的是默认配置的单次执行SimpleTrigger(未显式设置重复次数时默认重复0次,即仅触发1次),没有下一次触发时间,未开启恢复标记时会被直接判定为已完成清理。
解决方案
按以下步骤调整即可实现SimpleTrigger的故障转移:
- 第一步:构建JobDetail时显式开启故障恢复标记,代码示例:
JobDetail jobDetail = JobBuilder.newJob(ApiJob.class) .withIdentity("yourJobName", "yourJobGroup") .usingJobData("message", "yourParam") // 核心配置:标记任务执行中节点宕机后,需要其他存活节点重新执行 .requestRecovery(true) .build();
- 第二步:业务层增加幂等校验。开启
requestsRecovery后,故障节点上未执行完成的任务会被存活节点从头开始执行(不会保留之前的执行进度,比如例子中2分钟的sleep会重新计时),必须通过业务唯一标识做幂等,避免重复执行带来的逻辑异常。 - 第三步:按需调整集群检测间隔。当前
clusterCheckinInterval配置为20000毫秒即20秒,意味着节点宕机后最长需要20秒才会被集群识别,可根据业务对故障转移的时效要求,将该值调整为5000~10000毫秒,平衡故障转移时效与数据库查询负载。
内容的提问来源于stack exchange,提问作者sinha-shaurya
相关产品推荐
相关产品推荐

