一天跑三次的流水线,幂等比速度重要

我们的内容流水线一天触发三次,真正可怕的不是"没发出去",而是"发了两次"。重复发布一次,被冲掉的就不只是那篇内容,是整条流水线的可信度。

专属插画
一天跑三次的流水线,幂等比速度重要

一天跑三次的流水线,幂等比速度重要

我们的内容流水线一天触发三次,真正可怕的不是"没发出去",而是"发了两次"。重复发布一次,被冲掉的就不只是那篇内容,是整条流水线的可信度。

重复是从哪里来的

复盘一下背景:日更由 cron 兜底,09/14/20 三个时段,每次生成一篇 zh-CN 原文,再走三语发布。前几个星期一切正常,直到某个晚上网络抖动,脚本在上报阶段超时退出,cron 的补跑机制重新执行了一遍,结果同一天出现了两篇 slug 相同、正文相同的内容。

最初的修法是手动的:查一下发布记录,发现同一天两篇,手动把其中一篇删掉。删的过程中我们意识到,任何"失败了就人工兜底"的环节,都是重复的温床——人工清理没有统一标准,A 觉得该删的那篇,B 可能觉得是另一篇,越清越乱。

三道防线,从外到内

第一道是命名规范。同一天发布的文章共享同一个 slug 前缀,格式固定为八位日期。前缀的价值在于它是一枚可查询的指纹:发布之前先用它向 API 发一次"今天有没有已经发布的文章"。查到就核对,查不到才允许发布。这一步的成本是一次 GET 请求,换来的是查重不再依赖任何人的记忆。

第二道是发布前的闸口。查到已有同名文章时,脚本不自动跳过,也不自动顶替,而是给出三个出口:发布、HOLD、BLOCKED。HOLD 容易被忽略——直觉上重复就该删掉,但重复往往意味着有一个未知的任务实例跑过了,正确的动作是查它为什么跑,而不是删掉表面数据。

第三道是写入侧的兜底。发布脚本不靠"先查后写"防并发,而是把幂等交给数据库唯一约束,slug 加 locale 唯一,冲突时直接忽略写入。第二次调用不会报错,也不会覆盖第一次的结果,它变成一个无害的空操作。

两个踩过的坑

坑一:先查后写之间始终存在窗口。查重和写入之间隔着几毫秒,两个任务如果并发执行,都会通过"今天没有"的检查。这时唯一能拦住重复的只有数据库唯一约束,所以约束必须落在数据库层,不能只写在应用代码里。

坑二:校验失败不等于写入失败。早期我们把"校验没通过"当作重试信号,结果遇到一种情况:写入实际成功,只是缓存还没就绪,校验读了旧数据,触发重试,第二次写入又并发进来。后来我们把两件事拆开:写入状态只认数据库返回值;内容校验只做只读检查,失败了告警,绝不触发重写。

现在跑起来是什么样

这套机制上线后,三种失败模式各对位一个动作:查不到就发,查到就 HOLD,写入冲突就空操作。重复发布的次数归零,付出的成本是一次前置 GET、一条唯一约束、一个多出来的 HOLD 状态。

不吹高这套东西的技术含量,它全是朴素操作。真正值钱的那句话只有一句:让每个自动任务,执行两次和执行一次,结果一样。做到这一点,你才敢让它一天跑三次、一个月跑三十次,而不用留在工位听整晚的告警。

留言区

欢迎分享你的想法!

发表留言

0/500

加载留言中…