
本地测试一直正常的定时任务一上生产突然执行了三遍。代码里明明只有一个ScheduledCron也只有一份测试环境每天凌晨2点只跑一次。结果生产部署完成后第二天一看02:00:00 pod-a start dailyReport 02:00:00 pod-b start dailyReport 02:00:00 pod-c start dailyReport数据库里多了三份统计结果通知消息发了三遍第三方接口也被调用了三次。最麻烦的是代码里根本找不到“重复注册”。如果直接让 ChatGPT、Codex 看业务代码很容易把时间花在Cron表达式、方法调用甚至框架Bug上。但这种现象我一般先给一个核心判断代码只写了一份不代表线上只有一个执行者。如果本地只有一个实例而生产跑了3个Pod那么每个Pod都会启动自己的Scheduler。真正的问题很可能不在“任务写了几份”而在到底有多少个实例认为自己应该执行这份任务。一、先看表层原因是不是每个实例都在执行假设Spring里写了Scheduled(cron 0 0 2 * * ?)本地只有一个JVM自然每天只执行一次。生产环境如果是replicas 3真实运行状态其实是Pod A有一个SchedulerPod B有一个SchedulerPod C也有一个Scheduler。凌晨2点一到三个实例都会同时触发。所以第一件事不是改代码而是查日志里的Pod名称Instance IDHostnameTask ID执行时间如果三次执行分别来自三个不同Pod基本就能确定这是多实例重复调度不是单实例内部重复触发。二、如果只有一个实例也不要马上排除重复执行这里还有几个常见表层原因。第一种是失败重试。比如任务执行到一半数据库已经写成功但调用下游接口失败。任务框架认为本次执行失败于是重新跑一次。第二次执行时数据库又写了一遍。最后看起来像任务“执行两次”实际是第一次副作用已经发生但任务没有完成。第二种是任务注册了两次。例如两个Spring Context、重复Bean、旧任务没有移除或者同时存在应用内Scheduler和外部调度平台。所以排查时一定先回答重复来自不同实例还是同一实例这一步不清楚后面很容易修错方向。三、深一层看为什么“加锁”也不一定解决发现是多实例以后很多人的下一步就是加Redis分布式锁。方向没错但这里很容易只解决一半。例如任务平均30秒完成于是把锁TTL设成30秒。某天数据库变慢任务实际跑了90秒。结果就可能变成02:00:00Pod A抢到锁02:00:30锁自动过期02:00:31Pod B再次抢到锁A和B同时执行剩余逻辑。最后还是重复。所以分布式锁真正需要考虑的不只是有没有锁。还要看锁的生命周期能不能覆盖真实任务周期。包括异常情况。这也是为什么只让 ChatGPT、Codex 帮你“加一个Redis锁”很可能还不够。四、真实边界场景任务执行到一半实例突然挂了怎么办这个场景特别容易在线上踩坑。假设Pod A拿到锁后开始执行先写数据库。再发消息。最后调用第三方接口。执行到第二步时Pod突然被重启。这时候可能出现三种结果数据库已经改了。消息已经发了。任务平台却认为执行失败。下一次任务重新执行以后前两个动作可能再来一次。所以真正可靠的设计不能只依赖“任务只启动一次”。还应该保证任务即使重复执行业务结果也不会重复。这就是幂等。比如日报任务可以用report_date 2026-09-26作为唯一业务键。结算任务可以使用settlement_id作为幂等Key。数据库层再增加唯一约束。这样即使极端情况下调度真的重复了业务也不会真的生成两份结果。所以更稳的思路应该是调度层尽量避免重复业务层能够承受重复。五、最快排查顺序我一般按这6步走遇到“定时任务线上重复执行”可以直接这样查。第一步先确认执行者。把Pod、Hostname或者Instance ID打进日志判断重复来自不同实例还是同一实例。第二步确认线上实例数量。看Deployment、Pod数量不要拿本地单实例经验判断生产。第三步确认调度器是谁。到底是Scheduled、Quartz、XXL-JOB、K8s CronJob还是系统Crontab。不同调度器排查方式完全不同。第四步查互斥机制。如果多个实例都能触发是否有Redis锁、数据库锁、ShedLock或Leader Election。第五步查锁TTL和任务耗时。重点看P95、P99甚至异常情况下最长执行时间不要只看平均耗时。第六步查Retry和幂等。看看重复是不是失败重试导致的以及第二次执行会不会重复产生数据库、消息、通知或第三方副作用。这6步走完绝大多数重复定时任务问题基本都能定位到具体层面。六、具体怎么解决如果是普通Spring多实例服务里的Scheduled一种比较直接的方式是增加分布式互斥。例如使用ShedLock让所有实例都能触发但同一时间只有一个实例真正拿到执行权。不过这里要注意锁不是业务正确性的最后一道保险。如果任务涉及扣款结算发消息写报表生成订单调第三方接口最好再补业务幂等。也就是说锁解决“尽量不重复启动”。幂等解决“即使重复也不重复产生结果”。如果任务本身完全独立于业务服务也可以考虑把它拆成K8s CronJob让调度职责和业务服务分开。这时候再检查concurrencyPolicy防止上一轮还没结束下一轮又启动。七、修完以后怎么验证别只看“今天只跑了一次”这是很多文章容易漏掉的一步。真正验证时我建议至少跑4种场景。场景1三个实例同时触发预期结果应该是一个实例拿到执行权其他实例明确跳过。场景2任务执行时间超过平时故意把任务拖长检查锁会不会提前失效。场景3执行过程中实例崩溃看锁是否能恢复下一次任务能不能继续。场景4任务失败后Retry检查数据库、消息、第三方调用会不会重复产生副作用。最终不能只看“日志打印了一次。”还要看数据库最终只有一条结果。消息只产生一次有效业务事件。第三方操作没有重复执行。做到这里才算真正把问题闭环。八、这种开发场景Plus和Pro怎么判断如果平时只是偶尔让 ChatGPT、Codex看一次任务日志。分析Scheduled。帮你检查锁配置。补一个幂等判断。任务范围通常比较集中Plus一般已经够用。真正影响结果的反而是你有没有把这些Evidence准备好实例数量。Scheduler类型。任务日志。锁TTL。任务耗时。Retry机制。如果你的日常已经变成多个微服务都有定时任务。需要同时分析K8s、Redis、数据库、消息队列。一次任务还要让Codex跨多个文件修改、补并发测试、跑CI、做回归。并且一天连续处理很多类似工程问题这时候就属于高频、长时间、多轮工程工作流。这种使用强度下Pro会更合适。所以Plus和Pro不是看“这个Bug难不难”。而是看ChatGPT、Codex每天到底参与多少真实工程任务。偶发排查、短任务为主Plus通常够用。高频工程排障、长任务、多轮验证已经成为日常再考虑Pro。最后“定时任务明明只写了一份为什么线上执行了三遍”很多时候答案并不在Cron表达式里。而在代码虽然只有一份真正运行这份代码的实例却有三份。所以以后再碰到这个问题不要第一反应就是“是不是框架重复调度了”先确认谁执行的。几个实例。谁负责调度。有没有锁。锁会不会提前过期。失败后会不会Retry。业务能不能承受重复。真正稳定的线上任务不是依赖“它永远只会执行一次”。而是做到尽量只执行一次即使偶尔重复最终业务结果仍然正确。持续分享 Codex、大模型开发与 AI 编程实战内容。长期深度使用各类代码大模型也整理了稳定的Plus/Pro会员订阅渠道有需要可自取。