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

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

根因分析

问题由两个核心逻辑认知偏差导致:

  1. 失火策略作用范围不匹配
    配置的withMisfireHandlingInstructionFireNow()仅针对错过预定触发时间、尚未进入执行状态的失火触发器生效,对已经被节点拉取、处于执行中状态时节点宕机的任务完全不生效。从日志中Deleted 1 complete triggers(s).可以看出,故障扫描时集群直接把这个单次执行的SimpleTrigger标记为已完成删除,根本没有走到失火处理分支。
  2. 未开启任务恢复标记
    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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 19:39:38