這篇要回答的問題
HTTP request 進到 application 之後,要交給 message queue 非同步處理——producer 側的標準做法到底是哪幾步?哪一步少了會出事?
我把它整理成七站(加一個第 0 站的入口判斷)。這篇不重講每一站的原理,那些 queue 系列 19–25 篇已經各自寫過;這篇是把它們串成一條路的地圖,每站標出它防的那種事故,以及細節該去哪一篇看。
七站路徑
0. 該不該進 queue
要立刻拿到答案的不進;能「先受理、後處理」的才進。判斷框架見 async 工作的架構決策,naive event-driven 上線後會撞到什麼見 Production EDA 入門。
1. 同步段收 Idempotency-Key
防 client 重送:同一個 key 回同一個結果。(API 層冪等的做法見 冪等性)
2. 寫 DB + Outbox 同一個交易(核心)
handler 直接 publish 有兩種死法:寫了 DB 但 publish 掛掉(漏發),或 publish 了但交易 rollback(幽靈事件)。Outbox 讓真相留在 DB、由 relay 補發——這是 dual-write 的標準解(microservices.io: Transactional Outbox),細節見 Outbox pattern。
有了 Outbox,連 broker 整組掛掉重啟也只是「暫停」不是「掉單」。這句不是推論:我在 Express + RabbitMQ 實驗紀錄裡把三個 RabbitMQ 節點全殺掉再重啟,10 單全部 paid、零重複、outbox 全部發完;重啟期間吞吐會停,majority 回來後自己追上。
3. 回 202 Accepted + 查詢句柄
受理不該依賴非關鍵依賴:同一組實驗裡把 Redis 停掉,POST /orders 照樣回 202。
完成之後怎麼通知 client——走 status polling 或 webhook——是我的補充推論,還沒有在實驗裡驗證過(D 級)。
4. relay 發布三件套,缺一不可
durable queue + persistent message + publisher confirms。少了任何一個,broker 重啟或網路抖一下就可能默默丟訊息。RabbitMQ 文件明講「durable queue 與 persistent message 兩者都要做」才撐得過重啟(RabbitMQ: Queues),confirms 的語意見 RabbitMQ: Consumer Acknowledgements and Publisher Confirms。
5. envelope 帶穩定的 eventId
eventId 是消費端去重的 key。我踩過的坑:用 order_id 去重,結果同一張單的第二種事件(退款通知)被付款那筆誤擋掉。
envelope 另外帶 correlation ID、schema version——這兩個欄位是我的補充推論,實驗裡沒有驗證(D 級)。correlation ID 在追蹤上的用途見 EDA 端到端追蹤。
6. 經 EventBus Port 發布
業務碼不直接呼叫 broker SDK,而是透過一個 EventBus 介面(port)。換 broker 不動業務碼,測試時換成 FakeBus。
7. 消費側配套(同一條路的下半段)
prefetch 鎖住併發、處理成功才 ack、用 DB status 當冪等的真相、DLQ、快慢車道分流。分別見 Consumer 端冪等性、Retry 跟 DLQ,實測數字在 實驗紀錄。
我從這條路學到的
整條路壓成一行:受理(冪等)→ 同交易落地(Outbox)→ 202 → 可靠發布(三件套)→ eventId envelope → Port 隔離,下半段接消費側配套。
這七站不是風格偏好,每一站都對應一種具體的死法。想砍掉某一站之前,先回答一個問題:「這站防的那種事故,我打算怎麼扛?」答不出來就別砍。
另一個體會是:這條路的可信度,來自可以重跑的實驗,而不是背下來的 best practice。標準做法不用背——可以殺容器證明給自己看。(帳最後對不對,還有一道 對帳兜底。)