主备切换时定时任务重复执行源于角色未同步与状态未收敛,需从调度权、执行权、状态可见性三层面协同控制,结合共享存储校验、热备感知调度器、切换熔断及幂等续跑等机制保障一致性。

主备切换过程中定时任务重复执行,本质是“角色未同步”和“状态未收敛”导致的——主节点还没完全退出,备节点已抢先接管;或者两者都以为自己是主,同时触发同一套定时逻辑。这不是单纯加个锁就能解决的问题,得从调度权、执行权、状态可见性三个层面一起控制。
主备角色与任务执行严格绑定
不能让定时任务只认“时间到了”,而要让它先确认“我是不是当前合法的主节点”。常见做法是在应用启动或心跳检测时,主动向共享存储(如数据库配置表、ZooKeeper节点、Redis key)写入自己的身份标识和有效时间戳。任务触发前必须读取该标识,且校验其未过期、未被其他节点覆盖。一旦发现本地标识失效或冲突,立即放弃本次执行。
- 数据库方式:建一张active_node表,含node_id、role('master'/'standby')、last_heartbeat字段;定时任务每次执行前查最新 role=master 且 last_heartbeat 在 30 秒内的记录,只匹配本机 node_id 才继续
- Redis 方式:用 SET key value EX 30 NX 命令争抢 master 锁;成功者写入自身 IP + 启动时间,失败者不执行任务也不重试,等待下一轮心跳刷新
任务调度器本身支持热备感知
别用 Spring @Scheduled 这类纯本地调度器直接驱动核心业务任务。改用能识别主备状态的调度层,比如:
- ShedLock + 数据库锁表:它会在执行前尝试插入一条带唯一任务名的锁记录,插入成功才运行;主备共用同一张锁表,天然规避双主并发
- XXL-JOB 或 ElasticJob:它们自带注册中心和分片路由能力,主备节点注册到同一集群后,由调度中心统一分配执行权,备节点即使在线也不会被分配任务
- 自研轻量调度器:在 Quartz 的 TriggerListener 中注入主备校验逻辑,若当前非 master,则跳过 trigger,不进入 JobExecution
切换窗口期做任务执行熔断
主备切换不是瞬间完成的,中间存在几秒到几十秒的模糊期。这时要主动“让渡”执行权,避免争抢:
- 主节点在收到切换指令(如 keepalived notify script 触发)后,立即停止所有定时任务线程池,并清空待执行队列;同时更新数据库中的 active_node 状态为 'standby'
- 备节点在升主后,不立即执行任务,而是等待一个“冷静期”(如 5 秒),再检查自身是否已稳定持有 master 标识;期间所有到达的定时触发全部丢弃
- 对关键任务(如关单、对账)增加幂等判断:执行前先查数据库中该批次任务是否已有 SUCCESS 状态记录,有则直接跳过
失败任务自动续跑,不依赖固定节点
主备切换后,原主上未完成的任务不能丢失,也不能在新主上重复跑。解决方案是把任务执行过程拆成“可中断+可续跑”:
- 每个任务实例生成唯一 trace_id,执行进度(如已处理订单 ID 区间、最后偏移量)实时写入数据库任务快照表
- 新主节点启动后,扫描快照表中 status = 'RUNNING' 且 last_update_time 超过 2 分钟的任务,认为其已卡死,将其置为 'FAILED' 并触发补偿流程
- 补偿逻辑不是重跑全部,而是基于快照中的 offset 续跑剩余部分,确保数据不漏不重










