深浅主题
定时任务重复执行
问题现象
同一时间收到两条通知,批处理重复入账,重启后任务再次执行;每台实例的日志都只显示一次。
适用环境
Spring @Scheduled、多实例应用、MySQL 8;任务可能调用外部通知接口。
本文是可复用的排障示例,不代表某次个人生产事故。命令中的名称、账号和数据均为示例,执行前请替换为已确认的目标。
复现或诊断步骤
- 日志同时记录实例 ID、任务名、业务时段和执行 ID。
- 检查部署副本数、重复 Bean、多个调度器及任务重叠。
- 将任务触发与实际副作用拆开,检查重试和人工补发是否共享幂等键。
原因分析
@Scheduled 只负责单个应用进程中的调度,不提供集群唯一执行。即使有分布式锁,执行后响应丢失或锁过期仍可能导致重试,因此业务副作用还需要幂等。
修复示例
sql
-- 在独立迁移中创建,发布前保存数据库备份
CREATE TABLE task_slot (
task_name VARCHAR(80) NOT NULL,
slot_key VARCHAR(80) NOT NULL,
status VARCHAR(20) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (task_name, slot_key)
);
-- 尝试占用:重复键表示已有执行记录,需按状态处理
INSERT INTO task_slot(task_name, slot_key, status)
VALUES ('daily_summary', '2026-10-10T08:00+08:00', 'RUNNING');唯一键保护同一业务时段,执行记录应有成功、失败和超时恢复策略。不能把 RUNNING 永久当作成功;外部发送尽量携带相同幂等键,可靠通知可使用 outbox。变更前先设计失败重试和租约续期,回滚应用时保留执行记录防止重复。
验证方法
启动两个实例同时触发同一 slot,确认只产生一个业务结果。模拟进程在发送前后崩溃,检查恢复策略及重复发送边界;人工补发也必须可追溯。
常见误区
把 cron 秒数错开只降低冲突概率。分布式锁不是 exactly-once 承诺,数据库唯一键也无法独自消除没有幂等能力的外部系统重复副作用。