当前位置:首页 > 技术 > 正文内容

Quartz集群模式下重复调度问题的深度解析与解决方案

访客 技术 2026年8月31日 1

本文旨在深入探讨Quartz调度器在集群模式下偶尔出现的重复调度问题,并提供一套系统性的分析方法和解决方案。该问题自2019年起困扰内部系统,经过两年多的持续追踪和分析,终于有了清晰的结论。

问题排查历程

初期,我们尝试通过增加未调度任务提醒和重复调度告警来捕获问题。随着问题的持续出现,我们转向优化参数配置、监控系统负载,以排除因系统压力过大导致的异常。随后,我们着重调整了Quartz的Misfire(错过执行)策略,并参考了GitHub上的相关Issue,例如设置acquireTriggersWithinLock=trueacquireWithInLock。在最后阶段,我们不得已采取了在Quartz Listener中增加Misfire记录的手段,以精确记录问题发生的时间点。

系统化分析方法

在积累了详尽的日志数据后,我们得以将Misfire发生的时间点与Quartz的调度时间进行比对。通过深入分析Quartz源码中的applyMisfire方法,我们揭示了Misfire的判定规则。我们发现,在Misfire发生时,scheduleJob API的调用与Misfire的发生时间在逻辑上高度吻合,这强烈暗示了问题根源与scheduleJob API的使用方式有关。

Quartz重复调度的根本原因(集群模式)

在Quartz集群模式下,重复调度主要由以下两个因素引起:

  1. Quartz Issue #107:这是一个与并发处理相关的复杂问题。官方建议的解决方案是配置acquireTriggersWithinLock=true
  2. 不正确的Misfire处理导致的重复执行:这通常源于不当的Quartz API调用。

Quartz Issue #107详解

此问题的详细解释和解决方案请参考官方Issue。核心在于通过配置acquireTriggersWithinLock=true来确保在并发环境下触发器的获取是安全的。

API使用不当引发的Misfire

可以通过实现org.quartz.TriggerListener接口来监听triggerMisfired事件。结合对所有Quartz API调用进行埋点分析,可以定位到具体的问题场景。我们观察到一种常见模式:频繁地使用同一个TriggerKey修改Quartz调度器中的任务

根据Quartz API的实现,特别是org.quartz.Scheduler#scheduleJob(org.quartz.JobDetail, org.quartz.Trigger)方法,以及org.quartz.impl.triggers.CronTriggerImpl.java中的computeFirstFireTime方法:


    @Override
    public Date computeFirstFireTime(org.quartz.Calendar calendar) {
        nextFireTime = getFireTimeAfter(new Date(getStartTime().getTime() - 1000L)); // 基于启动时间计算下一个触发时间

        while (nextFireTime != null && calendar != null
                && !calendar.isTimeIncluded(nextFireTime.getTime())) {
            nextFireTime = getFireTimeAfter(nextFireTime);
        }

        return nextFireTime;
    }
  

当Cron表达式的启动时间(startTime)设置得非常早时,计算出的nextFireTime可能会早于"当前时间 - 阈值"。这会触发Quartz的Misfire检测流程,经过一系列内部调用(包括JobStore.acquireNextTriggersDefaultClusteredJobStore.applyMisfire),当满足getNextFireTime + threshold < now条件时,即会发生Misfire。此时,若Misfire指令被设置为MISFIRE_INSTRUCTION_FIRE_ONCE_NOW(例如CronTrigger的该指令),则会导致任务被立即重复调度。

值得注意的是,重复调度的间隔时间通常取决于调用Scheduler#scheduleJob API的时间与自然调度时间点之间的间隔。

解决方案与最佳实践

  1. 并发处理加锁:在复杂的并发场景下,务必考虑使用锁机制来保证操作的原子性。
  2. 正确使用Quartz API构建Trigger
    • 确保JobKeyTriggerKey的唯一性。
    • 在创建Trigger时,谨慎设置startAt时间。
    以下是推荐的代码示例:
    
    // 使用工具类生成 JobKey 和 TriggerKey
    JobKey jobKey = KeyUtil.jobKey(job);
    TriggerKey triggerKey = KeyUtil.triggerKey(job, schedule);
    
    // 创建触发器
    TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger()
            .withIdentity(triggerKey) // 设置唯一标识
            .forJob(jobKey)            // 关联Job
            .startAt(schedule.getStartTime()); // 必须谨慎设置启动时间!
    
    // 结合Cron表达式设置调度规则
    Trigger trigger = triggerBuilder.withSchedule(
        CronScheduleBuilder.cronSchedule("0 0/1 * * * ? *") // 示例Cron表达式
            .inTimeZone(TimeZone.getTimeZone("Asia/Shanghai")) // 指定时区
    ).build();
    
    scheduler.scheduleJob(trigger); // 调度任务
          

验证与示例

以下代码演示了当startAt时间被设置为一个很早的过去时间点时,如何触发Misfire:


import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.triggers.OperableTrigger;

public class MisfireTest {
    public static void main(String[] args) {
        // 设置一个较早的启动时间,例如,比当前时间早一秒
        LocalDateTime parse = LocalDateTime.parse("2021-11-30T15:00:00+08:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME);
        Instant hongkong = parse.toInstant(ZoneOffset.ofHours(8)).plusSeconds(1L); // 假设这里的时间是过去的时间
        Date startTime = Date.from(hongkong);

        // 创建触发器
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .startAt(startTime) // 关键:使用过去的启动时间
                .withDescription("Test NextFireTime @BySlankka")
                .withSchedule(CronScheduleBuilder.cronSchedule("0 0/1 * * * ? *") // 每分钟执行一次
                        .inTimeZone(TimeZone.getTimeZone("Asia/Shanghai"))) // 设置上海时区
                .build();

        // 计算第一个触发时间
        OperableTrigger operableTrigger = (OperableTrigger) trigger;
        Date nextFireTime = operableTrigger.computeFirstFireTime(null);

        // 格式化并打印结果
        String formattedNextFireTime = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
                .withZone(ZoneId.systemDefault())
                .withLocale(Locale.getDefault())
                .format(nextFireTime.toInstant());

        System.out.println("Calculated next fire time: " + formattedNextFireTime);
    }
}
  

上述代码输出的结果(例如:2021-11-30 15:01:00)表明,即使我们尝试设置一个过去的时间作为startAt,Quartz仍然会根据Cron表达式计算出下一个合法的触发时间。如果这个计算出的时间点仍然已经过去,就会触发Misfire机制。当Misfire指令设置为立即执行时,就会导致任务重复调度。

总结

Quartz虽然是成熟的调度框架,但在复杂的集群环境中,其细节不容忽视。本次对重复调度问题的分析揭示了Misfire处理和API使用的关键点。请注意,本文覆盖的场景并非全部,解决所有重复调度问题需要结合实际情况进行系统化分析。

标签: Quartz
返回列表

上一篇:在VS Code中配置Python、R和LaTeX开发环境

没有最新的文章了...

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。