結論先講
分散式系統只有三種訊息傳遞語意:At-Most-Once(可能丟)、At-Least-Once(可能重複)、Exactly-Once(理論上做不到)。 大部分系統選 At-Least-Once + Idempotent Consumer——接受重複,但讓重複處理的結果和只處理一次一樣。
坑 1:Exactly-Once Delivery 不存在
為什麼做不到
Producer → Broker → Consumer
步驟 1: Producer 送訊息給 Broker ✅
步驟 2: Broker 送訊息給 Consumer ✅
步驟 3: Consumer 處理完成,回 ACK 給 Broker
↑ 如果 Consumer 處理完了但 ACK 在網路中丟了呢?
→ Broker 以為沒處理 → 重送 → Consumer 處理了兩次
在分散式環境下,「送達」和「處理」和「確認」是三個不同的步驟,任何一步都可能失敗。你不可能同時保證「一定送到」和「只送一次」。
解法:Idempotent Consumer
不試圖保證「只處理一次」,而是保證「處理 N 次的結果和 1 次一樣」。
async function handleOrderCreated(event) {
// 用 event ID 做冪等檢查
const exists = await db.query(
'SELECT 1 FROM processed_events WHERE event_id = ?', [event.id]
);
if (exists) return; // 已處理過,跳過
await db.transaction(async (t) => {
await Product.decrement('stock', { ... }, { transaction: t });
await db.query(
'INSERT INTO processed_events (event_id) VALUES (?)', [event.id],
{ transaction: t }
);
});
}關鍵:用一個 processed_events 表記錄已處理的 event ID。處理和記錄在同一個 transaction 裡——要嘛都成功,要嘛都失敗。
坑 2:訊息順序不保證
場景
Event 1: order.created (10:00:01)
Event 2: order.cancelled (10:00:02)
你期望的順序:先 created 再 cancelled。
但 Kafka 的 partition 之間沒有順序保證。如果兩個 event 進了不同的 partition:
Partition 0: [order.cancelled] ← 先被消費
Partition 1: [order.created] ← 後被消費
Consumer 先收到 cancelled、再收到 created → 最終狀態是 created(應該是 cancelled)。
解法
1. 同一個 entity 的 event 送到同一個 partition
Kafka:用 orderId 當 partition key,保證同一筆訂單的所有 event 進同一個 partition。同一個 partition 內的順序是有保證的。
producer.send({
topic: 'orders',
messages: [{ key: orderId, value: JSON.stringify(event) }]
});2. Event 帶版本號
{ "type": "order.status_changed", "version": 3, "data": { "status": "cancelled" } }Consumer 只處理比目前版本大的 event,忽略舊版本。
坑 3:Consumer 掛了,怎麼恢復
Kafka 的 Offset 機制
Kafka 的 Consumer 用 offset 記錄「讀到哪裡了」。Consumer 掛了重啟,從上次 commit 的 offset 繼續讀。
但如果 Consumer 處理完了、offset 還沒 commit 就掛了呢?
→ 重啟後從上次 commit 的 offset 開始 → 重複處理(回到坑 1 的 idempotent 問題)。
RabbitMQ 的 ACK 機制
RabbitMQ 要求 Consumer 明確 ACK 才算消費完成。如果 Consumer 掛了沒 ACK,訊息會重新回到 queue 被其他 Consumer 拿走。
同樣的問題:處理完但還沒 ACK 就掛 → 重複處理。
結論
不管用 Kafka 還是 RabbitMQ,Consumer 都必須是 idempotent 的。
坑 4:Dead Letter Queue 必須有
什麼是 DLQ
Consumer 處理一個 event 失敗了(parse error、業務邏輯錯誤、下游服務掛了)。你可以:
- 無限重試:可能永遠成功不了,Queue 卡住
- 直接丟掉:資料遺失
- 送到 Dead Letter Queue:人工處理
DLQ 是「處理失敗的 event 的暫存區」。運維人員可以檢查 DLQ 裡的 event,修好問題後重新處理。
一定要設的原因
沒有 DLQ 的話,一個格式錯誤的 event 會讓 Consumer 進入「失敗 → 重試 → 失敗 → 重試」的無限迴圈,後面排隊的正常 event 全部被擋住。
坑 5:Event Schema 變更
場景
v1 的 event:
{ "type": "order.created", "data": { "amount": 1500 } }v2 改了結構:
{ "type": "order.created", "data": { "totalAmount": 1500, "currency": "TWD" } }如果 Producer 升到 v2 但 Consumer 還是 v1 → Consumer 找不到 amount 欄位 → 報錯。
解法:向後相容
1. 只增不刪
新增欄位沒問題,舊 Consumer 會忽略不認識的欄位。刪除或重命名欄位會破壞舊 Consumer。
2. 版本號
Event 帶 version 欄位。Consumer 根據 version 決定怎麼 parse。
3. Schema Registry
Kafka 有 Schema Registry,可以在 produce 時強制驗證 schema 相容性。不相容的 event 會被擋下來。
下一篇
框架選型方法論:不是選最快的,是選最適合的 — 團隊能力 × 硬體預算 × 生態需求 = 框架選擇。把壓測數據轉化成真正的決策方法論,不是比排名而是建決策框架。
本系列文章
完整 68 篇目錄見 系列首頁
← 上一篇:Event-Driven 架構入門:為什麼微服務離不開它 → 下一篇:框架選型方法論:團隊能力 × 硬體預算 × 生態需求