定时任务单机跑得好好的,一上多台机器就重复执行,到底该从哪拦?
定时任务在多台服务器上重复执行,根因通常不是代码写错,而是「任务」和「实例」之间没有绑定关系:每个实例都认为自己是唯一执行者。2026 年常见的处理顺序是:先确认这个任务是否真的需要多实例并行,再在调度中心分片、分布式锁、独立执行节点三条路里选一条,而不是先冲上去加锁。
为什么单机能跑,扩到多实例就开始重复
单机部署时只有一个进程持有定时器,任务天然只有一份,问题被掩盖了。一旦按 2026 年常见的做法把服务扩到两三个实例做高可用,每个实例启动时都会各自注册一遍定时器,同一个时间点就有了多份触发。如果任务涉及发券、扣库存、推送通知这类有副作用的写操作,重复执行一次就可能变成资损或者客户投诉。
动手改之前先分清两种重复。并发型是同一时刻几台机器几乎同时执行;补跑型是节点重启、调度器重试之后错峰触发。前者靠锁或者分片解决,后者要在调度层做「错过就跳过」的补偿判断,两者的处理位置并不相同。定位方法很直接——把执行日志里的实例标识打出来,看同一个业务键出现的次数和时间差。
- 并发型重复:同一秒内出现多条执行记录,多与锁竞争失败或者各实例各自触发有关。
- 补跑型重复:时间点分散,常与节点重启、调度器重试、任务超时判定有关。
- 看起来像重复的逻辑问题:任务本身没有幂等键,上游重复投递也会表现成同样的现象。
拦重复的三条常见路线,各拦得住什么
没有哪条路线能同时做到完全避免重复和高可用,只能取舍。分布式锁保证同一时刻只有一个实例执行,但锁租期、续期、网络分区没处理好的话仍会双跑;调度中心分片把任务按业务键分给不同实例,适合批量处理,代价是要求任务本身能按 key 切开;独立执行节点把定时逻辑从业务服务里剥离出去,部署简单,代价是多了一个需要盯着运维的组件。
- 分布式锁:改造工作量常见区间 1~3 人天,适合任务数量少、单次执行在 1 分钟以内的场景;要处理续期与释放,执行时长超过租期就会双跑。
- 调度中心分片:改造工作量常见区间 3~10 人天,适合对账、批量推送这类可按业务键拆分的任务;拆不开的任务用不上。
- 独立执行节点:工作量常见区间 1~2 人天,适合任务少、对高可用要求一般的内部系统;这台机器挂了任务就停,需要额外接监控。
选路线之前先问一句:这个任务重复执行一次,代价是什么?如果只是刷新一份缓存、重算一次统计,代价很低,就不必引入复杂机制;如果涉及资金、库存、对外通知,重复一次就要人工介入,那么「调度层选主 + 数据层唯一约束」两层都要上,只靠其中一层都不稳。
四步判断:这个任务该不该并发,唯一性放在哪层
下面这套顺序在项目交付里比较常用,核心逻辑是先定「要不要并发」,再定「怎么保证唯一」,最后才谈用哪个组件。反过来先挑中间件、再回头补语义,是返工的主要来源。
- 明确任务的业务键。比如按商户对账,业务键就是商户号加账期。没有业务键的任务,任何分布式方案都只能碰运气。
- 判断能不能并行。按业务键可切分的,优先走分片并行,吞吐更好;不可切分的全库汇总、单表迁移,就退回全局唯一执行。
- 选唯一性的兜底层。常见做法是调度层选主加数据层唯一约束双保险:调度层保证大概率只跑一次,唯一索引保证真重了也不会写脏。
- 补可观测。执行日志固定带任务名、实例标识、业务键、耗时、结果,缺一项,事后排查就得多花几个小时。
验收口径可以很直白:能写出业务键字段、能说清哪些键可以并行、能在数据库里找到对应唯一约束、能在日志平台按任务名查出最近一次执行。四项都做到,才算把重复执行这件事收住。
交付现场:抢锁失败的那次执行,日志去哪了
有个典型项目,客户预算紧、周期只有两周,三台应用实例上跑着四五个定时任务,最初的做法是加锁、租期设 30 秒。上线后碰上一个任务单次执行要跑 90 秒,锁提前过期,第二台机器又拿锁跑了一遍,对账结果翻倍。约束是预算不允许单独上一个调度组件,做法是把长任务拆成小批次、每批单独抢锁,同时打开锁续期,结果是数据不再翻倍,代价是多了一轮约两天的拆分改造和回归测试。后来复盘,这类长任务如果单次超过 1 分钟,拆批执行是常见区间;租期按 P95 的 2~3 倍设置也属于经验区间。
这里容易被忽略的是「抢锁失败要不要记日志」。如果只打一行 debug,事后根本分不清是「没触发」还是「抢锁失败」。按交付习惯,抢锁失败也应按 info 级别记一条,带上任务名和实例标识,确认今天到底跑了几次时可以直接数出来。
适用场景与边界
需要额外加协调机制的前提有两个:任务的副作用不可重复,而且服务确实部署了多份。反过来,如果任务只是重算一份幂等的聚合结果,或者服务本身就是单实例部署,那么加分布式锁、上调度中心都属于过度设计,只会多出运维面和排查成本。这条边界建议写进方案文档,免得后来者照着模板无脑加锁。
- 适合加协调机制:发券、扣减、对账、对外通知、批量数据迁移等有副作用的任务,且服务是多实例部署。
- 不必加:纯计算、可覆盖重算、缓存预热类任务,重复执行只是浪费一点资源。
- 先别急着上中间件:任务类型在 5 个以内、按天执行的,独立执行节点往往比引入调度中心更划算。
几个反复踩到的坑
- 锁的租期短于任务耗时:这是双跑的常见来源,经验做法是按任务 P95 耗时的 2~3 倍设租期,并开启自动续期。
- 只在任务开头加锁、写入前不校验持有者:锁过期后原实例还在写数据,需要在写之前确认自己仍持锁。
- 用「今天是否已执行」的标记表做控制却没有唯一索引:并发下两条查询同时判定未执行,照样跑两次。
- 任务里嵌套调用其他任务:调用链一长,锁的粒度就不清楚,容易出现互相等待或者漏锁。
常见问题
定时任务多实例就一定会重复执行吗?
不一定,取决于触发方式。由应用内定时器各自触发的会重复;由外部调度中心按实例分配触发的通常只跑一份,前提是分片配置正确。
只用数据库唯一索引、不加分布式锁,能顶住吗?
能挡住写脏数据,但挡不住重复计算和重复调用外部接口。有对外副作用的场景,建议唯一约束和调度层选主一起用。
分布式锁的租期设多长比较稳?
常见区间是按任务 P95 耗时的 2~3 倍设定并开启续期,同时给任务本身设一个超时上限,避免异常时长期占锁。
定时任务该不该和业务服务部署在同一批实例上?
任务数量少、耗时短的可以同部署;一旦出现长任务挤占资源或者锁竞争频繁,就建议拆到独立执行节点,减少互相影响。
怎么快速确认线上到底跑了几次?
在执行日志里固定打出任务名、实例标识和业务键,按业务键去重计数即可,比事后翻数据库结果表更直接。
如果你手上正好有多实例重复执行的任务,建议先别改代码,用一天时间把执行日志按任务名和实例标识统计一遍,确认是并发型还是补跑型,再按上面的四步走一遍。适用的前提是任务有明确业务键且副作用不可重复;如果任务本身可以幂等重算,把唯一约束补上通常就够了,不必额外引入调度组件。按 2026 年常见的交付节奏,一个中等规模系统做这类治理,经验上一到两周可以落地。
-
上传目录跟着代码一起发版,图片全丢了,文件到底该存本地还是对象存储?
日期:2026年9月13日 阅读:101
-
刚保存完跳详情页,读到的还是旧记录,读写分离是不是都得走主库?
日期:2026年9月12日 阅读:42
-
表刚上线时用自增主键挺省事,真到分库那天改起来有多麻烦?
日期:2026年9月10日 阅读:73
-
调大数据库连接池后接口反而变慢,连接数到底设多大合适?
日期:2026年9月9日 阅读:87
-
接口升级后老客户端用不了,旧接口要不要保留?
日期:2026年9月8日 阅读:115




