分布式定时任务怎么避免重复执行?多实例部署后 Cron 任务如何协调?
下面是一段教学用的模拟面试。
🧑💻 面试官:服务部署三个实例,每个都注册每日汇总任务,会怎样?
🙋♂️ 我:可能跑三次,用分布式锁限制一个实例执行。
🧑💻 面试官:任务没跑完,锁过期了,第二个实例接管。第一个实例又恢复了呢?
🙋♂️ 我:两个实例还是可能同时工作。
🧑💻 面试官:那怎样保证汇总结果不会重复写入?进程在完成工作后、更新任务状态前崩溃呢?
要分别处理“这一轮由谁执行”和“这次业务效果能否重复提交”。拿到锁,不等于任务已经具备恰好一次语义。
面试速答(60 秒版)
多实例定时任务需要明确调度入口,可以采用独立调度器、选主或任务领取机制,避免每个实例无协调地触发同一任务。
但调度只是一层。每轮任务还应有稳定身份,例如任务名和计划执行时间组成的唯一键,并持久化执行状态。
执行权可以用有限租约协调,超时后支持接管;同时要考虑旧执行者恢复的问题。必要时通过递增的 fencing token,让受保护资源拒绝过期执行者。
业务提交还要幂等或与任务状态正确衔接。重试、进程崩溃和补跑都可能重复执行,所以不能只凭一个分布式锁承诺恰好一次。

知识点详解:先固定任务身份,再讨论谁来跑
一轮任务,不能按收到通知的时刻认定
假设每天凌晨汇总前一天的数据。三个实例可能在稍有差异的时间收到调度通知,但它们指向同一轮汇总。
任务身份应当表达原本计划执行的那一轮,例如“每日汇总+业务日期”。不能把每次启动时的当前时间或随机 ID 当作唯一身份,否则重试会被当成新任务。
还要明确时区、边界日期和补跑规则。漏掉一天后是否补跑,以及补跑采用哪个数据截止点,都属于任务语义。

领取任务与执行任务可以分开
调度器把这一轮任务登记到持久化任务表,工作实例再竞争领取。唯一约束避免同一身份重复登记,原子领取或租约确定当前执行者。
任务表可以记录待执行、执行中、完成、失败,以及尝试次数和租约时间。这样进程崩溃后,其他实例能判断任务是否需要恢复,而不是依赖丢失的内存状态。
唯一登记只解决“一个任务记录”,不保证业务只执行一次。执行者可能在提交后崩溃,再被重新领取。
租约过期不意味着旧执行者停止
假设实例 A 因暂停或网络问题未续租,B 取得新租约开始工作。A 恢复后,未必知道自己已经失去执行权。
如果它仍然能向数据库或外部服务提交结果,就可能与 B 冲突。仅在开始时检查一次锁不够。
Fencing token 的思路是给每次新执行权一个递增编号,由真正接受写入的资源检查并拒绝旧编号。资源不支持这种检查时,不能只给请求加个数字就声称解决了问题。
业务幂等应该落到什么地方?
汇总结果可以用“业务日期+汇总维度”这样的唯一身份更新,而不是每次执行都新增一份。具体是否允许覆盖,还要看数据版本与业务规则。
任务状态和业务结果位于同一数据库时,可以考虑同事务提交。外部邮件、扣款等效果则需要对方的幂等接口、持久化发送流程或其他可恢复设计。
因此“不能同时跑”和“即使重跑也不会产生重复效果”是两个不同的保障。
使用 Kubernetes CronJob 也不能跳过这些问题
CronJob 的 Forbid 约束同一个 CronJob 创建的 Job 不重叠,不是给所有任务系统加了一把全局业务锁。它还可能跳过某些计划运行。
官方文档也提醒,调度在某些情况下可能创建两个 Job 或没有创建 Job,因此任务应设计成幂等。Kubernetes CronJob 文档
验证时应模拟重复调度、运行超过租约、旧执行者恢复,以及提交后崩溃。检查最终业务结果,而不是只看日志里有没有打印“获得锁”。
面试官继续追问
每次任务加一个随机 ID,可以去重吗?
只能区分尝试,不能识别它们是否属于同一轮任务。需要同时保留稳定的任务身份和各次尝试身份。
延长锁时间就够了吗?
不能保证。任务可能比预期更长,持有者也可能崩溃。租约、续约、接管和旧执行者约束要一起考虑。
所有定时任务都要这么复杂吗?
不用。可重复、低风险的清理任务可以采用更简单方案。复杂度应与业务效果、恢复要求和错误代价匹配。
面试速记卡
- 调度协调:避免每个实例无协调触发。
- 任务身份:用计划轮次和业务范围识别同一任务。
- 有限租约:支持失败接管,但旧执行者未必自动停止。
- fencing:需要写入资源真正拒绝旧执行权。
- 业务幂等:重跑不产生重复效果,不能由分布式锁替代。
公司面试真题
这道题暂未收录可核验的公司真题来源。你可以先阅读本文解析,或浏览已收录的公司面试真题。
浏览公司面试真题 →