Java 分布式定时任务重复触发与漏触发生产事故复盘:从 Cron 漂移到“锁+幂等+错过补偿”
技术主题:Java 编程语言
内容方向:生产环境事故的解决过程(故障现象、根因分析、解决方案、预防措施)
引言
定时任务在单机场景“看起来很简单”,但一旦进入多实例部署与复杂运行环境(容器弹性伸缩、时区/夏令时、下游波动),问题往往迅速复杂化。本次事故复盘,聚焦“同一 Cron 在多实例下重复执行、任务超时导致重入、misfire 导致漏触发”三个常见坑,并给出工程化治理方案与可落地代码骨架。
一、故障现象与影响
- 每天 00:05 的对账任务,偶发出现两次执行,产生重复对账记录;
- 在下游接口抖动时,任务执行时长超过一个周期,下一次触发重入,导致并发写入;
- 业务侧报警显示某些日期缺少对账记录,确认是错过触发(misfire)未补偿;
- 同一时间多个实例日志均记录“开始执行”,表明任务在集群中被多次调度。
影响:重复对账导致人工复核量上升;漏对账引发账务风险;CPU 与数据库写放大,影响同机业务。
二、排查路径
- 复现与定位:
- 抽取该任务近 30 天的执行日志,按分钟聚类,发现 00:05 同时有 2-3 个实例进入执行;
- 对比主机系统时间,发现一台实例 NTP 漂移 1.8 秒;
- 查看线程池与任务注解,使用的是 Spring @Scheduled,未加分布式锁;
- 检查 misfire 配置,未使用 Quartz,@Scheduled 默认无补偿;
- 业务库里存在同一账期重复写入,缺少幂等约束。
- 影响面评估:梳理所有 @Scheduled 任务,标注“有副作用/长时运行/跨日任务”,初步 7 个存在风险。
三、根因分析
- 集群唯一执行缺失:@Scheduled 在多实例下会在每个实例各自触发一次;
- 任务重入:执行超过一个周期时,下一次调度到来,发生并发执行;
- misfire 策略缺失:容器重启/GC 长停顿后错过触发未补偿;
- 时间漂移与时区:个别节点 NTP 偏移、时区未统一,造成触发边界差异;
- 业务幂等缺失:同一账期重复入库未做“幂等去重(唯一键/版本锁)”。
四、总体修复方案
- 集群唯一执行:
- 方案 A:Spring + ShedLock + Redis(或 JDBC)为每个任务加“分布式锁”。
- 方案 B:接入 Quartz 集群或调度平台(如 XXL-JOB),由调度器保证单次派发。
- 防重入与超时:配置“至多锁定时长”和“至少锁定时长”,避免重入与抖动导致抛锚;
- misfire 补偿:为关键任务启用 misfire 策略(FireAndProceed),或业务侧按游标补齐缺失账期;
- 幂等与去重:以业务键(账期 + 业务线)建立唯一约束;接入幂等表或去重缓存;
- 时间与时区:统一使用 UTC 存储,应用层以固定时区解析 Cron;全节点强制 NTP 同步;
- 观测与告警:暴露指标 last_run_time、last_success_time、duration、lag、fired_count、misfire_count,并对“超阈/缺失”告警。
五、关键代码与配置
1)Spring + ShedLock:集群唯一执行与防重入
pom.xml 依赖(示意):
1 | <dependency> |
配置与使用:
1 | // Java |
Redis 连接配置与 ShedLock 的配置略(生产建议设置合适的超时、序列化与连接池)。
2)Quartz:misfire 策略与禁止并发
1 | // Java |
quartz.properties
关键项(使用 JDBC 集群时):
1 | org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX |
说明:@DisallowConcurrentExecution
禁止同一 JobDetail 的并发;集群下由 Quartz 选举单实例触发;misfire 策略用于补偿错过的触发。
3)业务幂等:唯一键与去重表
1 | -- SQL(以账期 period + 业务线 biz 为唯一键) |
1 | // Java:写入前检查(或直接依赖唯一键 + 捕获违反异常) |
4)时间与时区、NTP
- 应用统一使用 UTC,Cron 显式指定 zone;
- 容器层启用 NTP/Chrony,偏移超阈告警;
- 夏令时切换窗口对跨日任务进行灰度与人工观察。
六、验证与观测
- 回放与对账:对近 60 天账期进行“缺失/重复”扫描,确保 0 漏 0 重;
- 压测与注故障:人为注入下游慢调用,验证“锁与幂等”在超时场景下生效;
- 指标看板:
- last_run_time / last_success_time / duration / fired_count / misfire_count;
- job_lag(当前时间-上次成功账期),超过阈值告警;
- lock_contention、idempotent_conflict 次数跟踪。
七、防复发清单
- 任何有副作用的 @Scheduled 必须加“集群唯一执行 + 幂等去重”;
- 关键任务必须有 misfire 策略或游标式补偿逻辑;
- 统一时区与 NTP,偏移超阈告警;
- 任务线程池与超时设置明确,禁止无限制并发;
- 上线前做“多实例演练 + 容器重启 + 时钟漂移 + 下游抖动”联合演练;
- 建立任务台账(负责人、周期、SLO、观测项和回滚方案)。
总结
定时任务的问题本质上是“调度正确性 + 执行有界性 + 结果幂等性”的系统工程。在多实例与不可靠环境下,仅靠 @Scheduled 难以满足生产要求。通过“锁住触发(ShedLock/Quartz 集群)+ 幂等去重 + misfire 补偿 + 统一时钟 + 可观测性”,可以把重复和漏触发的风险降到可控范围内。把这些做成模板化、平台化能力,才能从根上减少此类事故反复发生。