定时任务的分布式调度:避免重复执行
雪花毛衣
5 min read
判断:单机 cron 在集群里会每个节点都跑一遍,重复执行轻则重复发邮件,重则重复扣款。分布式调度要把"谁来执行"和"执行一次"这两个问题同时解决,靠分布式锁或中心化调度器,而非祈祷只部署一个节点。
为什么单机 cron 不行
部署了 3 个实例,3 个实例的 cron 同时触发,任务跑 3 次。有人用"只部署一个节点"规避,但那个节点挂了任务就停摆,且扩缩容时极易误配。这不是调度,是侥幸。
方案 A:分布式锁选主
任务触发时,所有实例去抢一把分布式锁,抢到的执行,没抢到的跳过:
ok := tryLock("job:send-report", 10*time.Minute)
if !ok { return } // 别的实例在跑,退出
defer unlock()
// 真正执行任务
锁要有合理超时:任务正常几分钟跑完,锁超时设 10 分钟;若任务真的超时被解锁,另一实例又抢到开始跑,会造成重叠。所以任务本身也该幂等,重叠时不出错。
方案 B:中心化调度器
用调度框架(如基于 Quartz 集群、或专职调度服务)统一管理触发,由调度器决定派发给哪个 worker,从架构上保证"同一任务同一时刻只派一发"。worker 只管执行,不用自己抢锁。中心化方案还支持任务分片:一个大数据量的任务按分片参数(如按用户 ID 取模)分给多个 worker 并行,既避免重复又提升吞吐。
调度器 → 任务分10片 → worker1 片[0..2] / worker2 片[3..5] / ...
分片参数由调度器下发,每个 worker 只处理自己那片,天然不重复。
错过执行的补偿
调度器或 worker 宕机期间该跑的任务没跑,恢复后要决定:
- misfire 忽略:错过就错过,等下一周期。
- 立即补跑:恢复后把漏掉的跑掉,适合不能漏的账务类任务。
- 按频率合并:漏多次只跑一次。
选择取决于任务语义,不能默认。补跑本身也可能和下一周期撞车,需配合幂等和分片去重。
执行超时与中断
任务卡死(如死循环、下游无响应)会一直占着 worker。调度器应设任务级超时,超时后发中断信号,worker 捕获后清理退出。对不响应中断的阻塞调用(如同步 socket),超时机制要能强制终止进程或隔离执行,否则一个坏任务拖垮整个调度节点。
分片广播与任务编排
除"选主执行一次"外,还有两类常见模式。分片广播:调度器把任务拆成若干片,下发到所有 worker,每个 worker 处理自己负责的分片,适合"每台机器清理自己本地日志"这类本地的、可并行的任务。广播模式天然不重复,因为每片只在一台执行,但要防止 worker 数量变化时分片漂移导致漏片或重片,分片算法要稳定。
任务编排则是多个定时任务间有依赖:A 跑完 B 才能跑。简单依赖可用"B 等 A 产出的标记或状态"来串,复杂依赖应交由调度框架的有向无环图能力,由框架保证前置成功才触发后继,并把失败节点隔离重跑,而不是手写一堆互相轮询的标志位。
任务执行的可重入设计
幂等不是只靠锁,更要靠"已处理状态"持久化。任务开头先查状态表:若本批次已处理,直接返回;否则处理并落状态。这样即使调度器重发、锁重叠、人工补跑,都不会产生重复副作用。状态表自身要建唯一索引,并发重入时由数据库兜底。
踩坑
锁超时小于任务时长:任务没跑完锁先释放,第二个实例进场,重复执行。锁超时取"任务正常耗时的数倍",并让任务幂等兜底。
依赖机器本地时钟触发:各节点时钟不齐,触发时刻漂移,抢锁竞争更激烈。触发以调度器时间为准,而非各节点本地时间。
任务不是幂等的:即便有锁,重叠或重跑都会出错。定时任务应设计成可重入——用状态表记录已处理批次,重复触发跳过已处理部分。
没有执行日志与告警:任务悄悄没跑,几天后才被发现。每次执行记录起止、结果,失败告警。
只部署单节点规避重复:单点故障导致任务停摆,且扩缩容易误配。用分布式锁或中心化调度才是正解。
1 view
0 likes
0 comments