一天跑三次的流水線,冪等比速度重要
我們的內容流水線一天觸發三次,真正可怕的不是「沒發出去」,而是「發了兩次」。重複發布一次,被沖掉的不只是那篇內容,是整條流水線的可信度。

一天跑三次的流水線,冪等比速度重要
我們的內容流水線一天觸發三次,真正可怕的不是「沒發出去」,而是「發了兩次」。重複發布一次,被沖掉的不只是那篇內容,是整條流水線的可信度。
重複是從哪裡來的
復盤一下背景:日更由 cron 兜底,09/14/20 三個時段,每次生成一篇 zh-CN 原文,再走三語發布。前幾個星期一切正常,直到某個晚上網路抖動,腳本在上報階段超時退出,cron 的補跑機制重新執行了一遍,結果同一天出現了兩篇 slug 相同、內文相同的內容。
最初的修法是手動的:查一下發布紀錄,發現同一天兩篇,手動把其中一篇刪掉。刪的過程中我們意識到,任何「失敗了就人工兜底」的環節,都是重複的溫床——人工清理沒有統一標準,A 覺得該刪的那篇,B 可能覺得是另一篇,越清越亂。
三道防線,從外到內
第一道是命名規範。同一天發布的文章共享同一個 slug 前綴,格式固定為八位日期。前綴的價值在於它是一枚可查詢的指紋:發布之前先用它向 API 發一次「今天有沒有已經發布的文章」。查到就核對,查不到才允許發布。這一步的成本是一次 GET 請求,換來的是查重不再依賴任何人的記憶。
第二道是發布前的閘口。查到已有同名文章時,腳本不自動跳過,也不自動頂替,而是給出三個出口:發布、HOLD、BLOCKED。HOLD 容易被忽略——直覺上重複就該刪掉,但重複往往意味著有一個未知的任務實例跑過了,正確的動作是查它為什麼跑,而不是刪掉表面資料。
第三道是寫入側的兜底。發布腳本不靠「先查後寫」防併發,而是把冪等交給資料庫唯一條件約束,slug 加 locale 唯一,衝突時直接忽略寫入。第二次呼叫不會報錯,也不會覆蓋第一次的結果,它變成一個無害的空操作。
兩個踩過的坑
坑一:先查後寫之間始終存在視窗。查重和寫入之間隔著幾毫秒,兩個任務如果併發執行,都會通過「今天沒有」的檢查。這時唯一能攔住重複的只有資料庫唯一條件約束,所以約束必須落在資料庫層,不能只寫在應用程式碼裡。
坑二:校驗失敗不等於寫入失敗。早期我們把「校驗沒通過」當作重試信號,結果遇到一種情況:寫入實際成功,只是快取還沒就緒,校驗讀了舊資料,觸發重試,第二次寫入又併發進來。後來我們把兩件事拆開:寫入狀態只認資料庫回傳值;內容校驗只做唯讀檢查,失敗了告警,絕不觸發重寫。
現在跑起來是什麼樣
這套機制上線後,三種失敗模式各對位一個動作:查不到就發,查到就 HOLD,寫入衝突就空操作。重複發布的次數歸零,付出的成本是一次前置 GET、一條唯一條件約束、一個多出來的 HOLD 狀態。
不吹高這套東西的技術含量,它全是樸素操作。真正值錢的那句話只有一句:讓每個自動任務,執行兩次和執行一次,結果一樣。做到這一點,你才敢讓它一天跑三次、一個月跑三十次,而不用留在工位聽整晚的告警。
留言區
歡迎分享你的想法!
載入留言中…