自动化脚本重复执行怎么排查?
自动化系统越大,越要避免单点脚本承担全部职责
深夜两点,值班手机突然疯狂震动,监控大屏上原本平稳的曲线瞬间飙升。查了半天,发现某个核心业务数据居然出现了双份,或者是库存被扣减了两次。这时候你可能会疑惑:脚本明明返回了成功退出码,怎么还会搞出这种幺蛾子?其实,这类“自动化脚本重复执行”引发的事故,往往比脚本直接报错更隐蔽,破坏力也更强。排查这类问题,不能只盯着日志里的“Exit 0”看,得像个刑侦专家一样去现场寻找蛛丝马迹。
寻找“案发”现场的蛛丝马迹
排查的第一步,得先确认是不是真的发生了重复执行。很多时候,脚本日志看起来一切正常,是因为脚本本身跑得好好的,问题出在“触发源”。
- 时间戳重叠分析:把脚本执行的开始和结束时间拉出来做个散点图。如果发现两个任务的时间区间有重叠,或者间隔短得离谱(比如一个耗时5分钟的任务,1分钟后又启动了一次),那多半是调度系统没忍住,或者是上一轮还没结束下一轮就强行启动了。
- 外部系统的“回声”:查一下API调用记录。如果同一个业务单号在短时间内发起了两次请求,哪怕返回码不同(比如第一次超时,第二次成功),也足以证明上游并没有做好幂等控制。
- 临时文件的残留:很多脚本喜欢用临时文件做锁或标记。如果发现脚本启动时报“文件已存在”,但进程列表里却找不到正在跑的同类,那很可能是上次跑崩了没清理干净,或者是两个脚本实例在争抢同一个锁文件。
深入代码逻辑的“黑洞”
确认了重复执行的事实,接下来就得钻进代码里看看到底哪里漏了风。最常见的问题往往出在对“状态”的盲目自信。
很多脚本默认自己是“上帝视角”,觉得只要拿到了任务,这任务就一定是“待处理”状态。实际上,在并发场景下,当你读库的一瞬间,数据可能已经被别的进程改写了。排查时要重点看那些读后写的操作:是不是先Select了一条状态为“Pending”的记录,Update成“Processing”,然后处理完再Update成“Done”?如果这中间没有数据库层面的乐观锁(比如Version字段)或者悲观锁,那两个脚本同时Select到同一条记录就是分分钟的事。
还有一种隐蔽的情况是副作用外溢。脚本里可能有一段逻辑是“发送通知邮件”。如果没有去重机制,脚本重试一次,用户邮箱里就会多躺一封邮件。这种问题在测试环境根本测不出来,只有到了生产环境,面对真实的用户投诉才会暴露。
别被“成功退出”骗了
很多人排查脚本问题时,习惯性地看退出码。只要脚本返回0,就觉得万事大吉。这其实是个巨大的误区。
脚本的“成功退出”,仅仅代表代码跑完了所有指令,没有抛出未捕获的异常。它并不代表业务逻辑是正确的,更不代表没有产生副作用。比如一个脚本原本要处理100条数据,结果因为网络抖动只处理了50条,然后正常退出了。这时候监控系统一片祥和,但业务已经受损。排查时,得在脚本内部埋点,记录下实际处理的数据量,和预期值做对比。如果发现处理量对不上,哪怕退出码是0,也得当成事故来处理。
构建防重执行的“护城河”
排查清楚问题后,更重要的是如何避免下次踩坑。与其在脚本里打补丁,不如在架构设计上就做好隔离。
唯一ID是命门。不管是订单号、任务ID还是流水号,必须确保全局唯一。有了这个ID,无论是写日志、加分布式锁还是做数据库唯一索引约束,都有了抓手。脚本启动第一件事,应该是拿这个ID去“占坑”,占到了才处理,占不到就直接退出,这才是最稳妥的逻辑。
状态机要闭环。把业务状态流转画出来,明确哪些状态可以互相转换。比如“已支付”只能流向“发货中”,绝不能流向“已取消”。在数据库层面用状态字段做约束,脚本执行时带上状态条件更新(UPDATE ... WHERE status = 'PAID'),这样就算脚本重复执行,第二次也会因为状态不匹配而无效。
自动化脚本虽然写着简单,但要让它经得起生产环境的折腾,光靠“跑通”是远远不够的。下次再遇到脚本重复执行的问题,别急着骂娘,按着这些路子一点点排查,总能找到那个藏得最深的Bug。

参与讨论
调度系统是不是没做互斥锁啊?