我正在處理一個生產工作流程,其中外部 API 有時會重試相同的 webhook,所以 n8n 可能在幾秒鐘內收到重複的事件。該工作流程會建立資料庫記錄並觸發下游操作,因此僅檢查執行歷史似乎不夠可靠。
我正在考慮使用 PostgreSQL 搭配唯一的事件 ID,以及在繼續工作流程之前執行 INSERT … ON CONFLICT DO NOTHING。
INSERT INTO webhook_events (event_id, received_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
這是一個好的模式嗎,還是你會以不同的方式處理去重?
描述問題/錯誤/問題
錯誤訊息是什麼(如有)?
請分享你的工作流程
(選擇畫布上的節點,並使用鍵盤快捷鍵 CMD+C/CTRL+C 和 CMD+V/CTRL+V 來複製和貼上工作流程。)
分享最後一個節點返回的輸出
關於你的 n8n 設定的資訊
- n8n 版本:
- 資料庫(預設:SQLite):
- n8n EXECUTIONS_PROCESS 設定(預設:own、main):
- 執行 n8n 的方式(Docker、npm、n8n cloud、桌面應用程式):
- 作業系統:
你好 @Sophia_Bennett 是的,我會使用資料庫作為真實來源,而不是依賴 n8n 執行歷史記錄。
唯一的 event_id 加上 INSERT … ON CONFLICT DO NOTHING 可以提供原子性的去重檢查,即使多個工作者幾乎同時接收到相同事件也不例外。
INSERT INTO webhook_events (event_id, received_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
如果沒有返回任何行,請將其視為重複事件並停止工作流程。我還會在 event_id 上新增索引/唯一約束,並保留去重記錄的時間與提供者可以重試事件的時間一樣長。
對於更關鍵的工作流程,我還會追蹤狀態(處理中、已完成、失敗),以便重試可以安全地恢復,而不會造成重複的副作用。
嗨 @Sophia_Bennett 歡迎加入社群!
是的,那是正確的做法。ON CONFLICT DO NOTHING 是不可分割的,所以兩個同時進行的遞送不能同時通過。要使其安全用於生產環境:
- 把守:在插入後添加一個 If 節點:
{{ $json.event_id }} 不為空。重複項會返回無行並停止在那裡。
- 故障:如果工作流在插入後失敗,重試會被視為重複項而被丟棄,事件遺失。添加一個
status 欄位,在末尾將其設置為 done,並在發生錯誤時刪除或將行標記為 failed,以便重試可以通過。
- 立即在 Webhook 節點中響應。響應緩慢是觸發大多數重試的原因。
- 清理超過幾天的行,使用計劃工作流。
嗨 @Sophia_Bennett,是的,有一個非空唯一的 event_id 是一個很好的原子閘門。但不要在每次失敗時刪除事件列,因為下游 API 可能在 n8n 逾時前完成動作,所以重試可能會重複執行。
如果你的業務記錄在同一個 PostgreSQL 資料庫中,請將事件 ID、業務寫入和下游工作的寄件箱列一起提交。透過 Postgres Execute Query 呼叫的預存程序可以做到這一點,僅在事件插入成功時套用寫入。之後透過 Respond to Webhook 返回 200。
分別處理寄件箱,針對每個事件/動作重複使用穩定的冪等性金鑰(如果接收 API 支援的話)。否則,在重試前檢查不確定的結果。為提供者的重試/重播視窗保留事件 ID,而不是任意幾天。
在重複路徑上有一個 n8n 特定的細節,因為 If 閘門和「在提交後回應 Webhook」都依賴它:
當 ON CONFLICT DO NOTHING … RETURNING 碰到重複項時,Postgres 節點輸出零個項目,預設情況下 n8n 簡單地不會在沒有輸出的節點之後執行任何操作。所以 If 節點永遠看不到重複項,如果「回應 Webhook」位於該分支上,重複傳遞可能永遠無法獲得乾淨的 200 回應。對於在非 2xx 或超時時重試的提供商,這可能會將一個重複項變成多個。
在 Postgres 節點的設定中開啟「Always Output Data」可以修復此問題:該節點在重複項時會傳遞一個空項目,event_id 上的 If 會捕捉它,你可以直接將重複分支發送到自己的「回應 Webhook」並返回 200,以便提供商停止重試。
是的,那是一個堅實且廣泛使用的模式。
推薦做法
INSERT INTO webhook_events (event_id, received_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
然後在 n8n 中:
• 如果查詢返回一行 → 這是你第一次看到該事件 → 繼續工作流程。
• 如果返回無結果 → 這是一個重複事件 → 停止工作流程(或路由到「重複」分支)。
Hi @Sophia_Bennett 是的,這是一個很強的生產環境模式。我會讓 PostgreSQL 成為去重複化的單一信息源,而不是依賴 n8n 的執行歷史。
重要的部分是唯一約束:
CREATE UNIQUE INDEX ux_webhook_events_event_id
ON webhook_events (event_id);
那麼你的插入就變成了原子操作:
INSERT INTO webhook_events (event_id, received_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
如果 RETURNING 返回了 event_id,這次執行就成功地聲稱了該事件,可以繼續。
如果它返回空值,另一次執行已經處理或聲稱了該事件,因此工作流應該在創建記錄或觸發下游操作之前停止。
Webhook
↓
插入事件 ID
↓
行是否返回?
├─ 是 → 處理事件
└─ 否 → 重複 → 停止
還要避免將去重複化檢查和實際業務操作放在分開的「先檢查後插入」步驟中。兩次 n8n 執行可以同時通過該檢查。讓 PostgreSQL 的唯一約束來處理競態條件。
還有一點很重要:去重複化不會自動使下游操作具有冪等性。如果工作流發送付款、電子郵件、API 請求等,我會使用相同的事件 ID 作為冪等性鍵,只要下游系統支持它。