cover

API 必須 200ms 內回應,但同時要寄信、寫分析、觸發 webhook。如果全部同步做,回應時間直接飆到 2 秒。如果其中一個服務掛了,整個 request 就失敗。Queue 的目的不是把問題藏起來,是把「不可控」變成「可控」。

先講結論

同步只做核心成功(扣款、下單),其餘交給 Queue。Consumer 必須冪等(重複處理不出錯)。失敗的事件進 DLQ(Dead Letter Queue),不能直接消失。沒有監控的 Queue 比沒有 Queue 更危險。

同步 vs 非同步:先劃清邊界

簡單的判斷:使用者要立刻知道結果的 → 同步。失敗可延後處理的 → 非同步。

  • 同步:扣款、權限驗證、下單確認
  • 非同步:寄信、推播通知、分析事件、對外 webhook

原則是 API 只做「核心成功」,剩下的事情發一個 event 就好。使用者不需要等你寄完信才看到「下單成功」。

冪等:讓重複也不出錯

大部分 Queue 系統只保證 at-least-once——訊息可能被送兩次。如果 consumer 不是冪等的,你就會寄兩封信、扣兩次款。

CREATE TABLE event_consumed (
  event_id TEXT PRIMARY KEY,
  processed_at TIMESTAMPTZ DEFAULT now()
);
 
-- 消費前先登記,已存在就跳過
INSERT INTO event_consumed (event_id)
VALUES (:event_id)
ON CONFLICT DO NOTHING;

事件要有唯一 event_id,consumer 先查有沒有處理過,處理過就直接 ACK。簡單但救命。

Outbox Pattern:避免「DB 寫了但事件沒發」

你在同一個 request 裡要做兩件事:寫 DB + 發事件。如果 DB 寫成功但事件發失敗呢?資料已經變了,但下游不知道。

Outbox Pattern 的解法:把事件也寫進同一個 DB transaction 的 outbox table,然後由背景 worker 去讀 outbox 發送事件。因為在同一個 transaction 裡,要嘛一起成功、要嘛一起失敗。

Retry 和 DLQ:失敗不是結束

第一次失敗 5 秒後重試、第二次 30 秒、第三次 5 分鐘。超過次數就進 DLQ。

DLQ 不是垃圾桶。 裡面的事件都是需要人工介入的。你要有流程去 review DLQ:為什麼失敗?修完 bug 後能不能重新送回 Queue?如果 DLQ 堆了幾百筆沒人管,那等於你有幾百筆業務丟失了。

技術選型:從簡單開始

Redis Queue(BullMQ):最快上手,你多半已經有 Redis。適合小團隊、低流量。

import { Queue, Worker } from 'bullmq'
 
const queue = new Queue('email', {
  connection: { host: 'redis', port: 6379 },
})
 
await queue.add('send', { to: 'user@acme.com' }, { attempts: 3 })
 
const worker = new Worker('email', async job => {
  // send email
}, { connection: { host: 'redis', port: 6379 } })

RabbitMQ:需要 routing、ack、延遲、priority 的場景。功能比 Redis 強大,但多一個服務要維護。

Kafka:高吞吐、可 replay。適合事件溯源和大規模系統。但部署和維護成本高,10 人團隊不需要。

建議順序:Redis → RabbitMQ → Kafka。 不要因為履歷好看就直接上 Kafka。

Backpressure:Queue 越來越長怎麼辦?

Queue 長度持續增加 = 生產速度 > 消費速度。不處理的話,記憶體會爆、延遲會飆。

應對方式:增加 worker 數量(最直接)、降低 producer 速率(限流)、監控 queue length 並告警。如果 queue 長度超過 1000 且持續 10 分鐘,你應該要知道。

事件命名:發布了就是契約

order.createdinvoice.paid —— domain.action 格式。加上 schema_version: 2 做版本管理。

事件一旦發布就是契約,不能隨便改格式。consumer 依賴這個格式,你改了它就壞了。要改就加新版本,讓 consumer 有時間遷移。


Queue 就像餐廳的出菜口。廚師做完菜放上去,服務生自己來拿。廚師不用等服務生、服務生不用等廚師,兩邊各自忙各自的——但前提是出菜口不能塞住。