結論先講
在做單體的時候,你完全不會覺得需要 event-driven。 拆成微服務之後才發現:沒有 event,跨服務的資料一致性根本做不到。這不是「要不要用」的問題,是「不用就做不下去」。
單體 vs 微服務:同一件事的做法
單體:一個 function call
// 單體:訂單 + 扣庫存 + 通知,一個 transaction
async function createOrder(data) {
const t = await db.transaction();
try {
const order = await Order.create(data, { transaction: t });
await Product.decrement('stock', { where: { id: data.productId }, transaction: t });
await Notification.create({ userId: data.userId, message: '訂單成立' }, { transaction: t });
await t.commit();
return order;
} catch (e) {
await t.rollback();
throw e;
}
}簡單、直覺、一個 transaction 保證全部成功或全部失敗。
微服務:沒辦法一個 transaction
Order Service、Product Service、Notification Service 在不同的 process,甚至不同的機器。你不能跨 process 做 DB transaction。
如果用 REST 同步呼叫:
Order Service → POST /products/decrement-stock → Product Service
→ POST /notifications → Notification Service
問題:如果扣庫存成功但通知失敗,怎麼 rollback?如果 Product Service 暫時掛了呢?
答案就是 Event-Driven。
Event-Driven 怎麼運作
Order Service:
1. 建立訂單(status: pending)
2. 發布事件 → "order.created" { orderId: 123, productId: 456 }
Product Service:
1. 收到 "order.created"
2. 扣庫存
3. 發布事件 → "stock.decremented" { orderId: 123, success: true }
Notification Service:
1. 收到 "order.created"
2. 發通知給用戶
Order Service:
1. 收到 "stock.decremented"
2. 更新訂單狀態 → confirmed
每個 service 只做自己的事,透過 event 通知其他 service。沒有跨服務的 transaction,只有各自的 local transaction + event 串接。
如果某一步失敗呢
Product Service 扣庫存失敗(庫存不足):
Product Service:
發布事件 → "stock.decrement_failed" { orderId: 123, reason: "out_of_stock" }
Order Service:
收到 "stock.decrement_failed"
更新訂單狀態 → cancelled
(可選)發布事件 → "order.cancelled" 讓其他 service 做補償
這就是 Saga Pattern(第 46 篇 會深入講)。
Event Schema 設計
好的 event
{
"type": "order.created",
"version": "1.0",
"timestamp": "2026-04-15T10:30:00Z",
"source": "order-service",
"data": {
"orderId": "abc-123",
"userId": "user-456",
"productId": "prod-789",
"quantity": 2,
"totalAmount": 1500
}
}包含:type(什麼事)、version(schema 版本)、timestamp(什麼時候)、source(誰發的)、data(具體內容)。
壞的 event
{
"action": "create",
"table": "orders",
"row": { "id": 123, "user_id": 456, ... }
}這不是 event,這是 DB changelog。Consumer 需要知道你的 DB schema 才能處理——這就是 第 29 篇 說的「把 Kafka 當 REST 用」。
Event 命名規則
{domain}.{action}
order.created ✅ 清楚
order.cancelled ✅ 清楚
order.updated ⚠️ 太模糊(更新了什麼?)
order.status_changed_to_shipped ✅ 具體
db.orders.insert ❌ 這是 DB 操作不是業務事件
Kafka vs RabbitMQ:選哪個
| 維度 | Kafka | RabbitMQ |
|---|---|---|
| 核心模型 | Log(持久化、可重播) | Queue(消費後刪除) |
| 吞吐量 | 極高(百萬 msg/s) | 高(萬~十萬 msg/s) |
| 延遲 | 中(ms~幾十ms) | 低(sub-ms) |
| 重播 | 可以(Consumer 倒帶) | 不行(消費就沒了) |
| 路由 | 簡單(Topic + Partition) | 豐富(Exchange + Routing Key) |
| 學習成本 | 高(Partition、Offset、Consumer Group) | 中 |
選 Kafka 的場景
- 需要事件重播(「上個月的訂單事件重跑一次」)
- 超高吞吐量(日誌收集、IoT 數據)
- 事件要保留很久(audit log)
選 RabbitMQ 的場景
- 複雜的路由規則(不同 event 送到不同 queue)
- 需要低延遲(sub-ms)
- 團隊更熟悉傳統 message queue
選 Redis Pub/Sub + BullMQ 的場景
- 已經有 Redis
- 規模不大(幾千 msg/s)
- 不需要持久化(丟了就算了)
回顧 第 27 篇:大部分中小規模的微服務用 BullMQ(Node.js)或 Laravel Queue 就夠了,不需要上 Kafka。
下一篇
Event-Driven 的坑:Exactly-Once 不存在、順序不保證 — 理論上 event-driven 很美好,實務上到處是坑。訊息重複怎麼辦?順序錯亂怎麼辦?Consumer 掛了重啟後從哪裡開始?Dead Letter Queue 為什麼必須有。
本系列文章
完整 68 篇目錄見 系列首頁
← 上一篇:Cache 的常見誤用:你可能正在犯的五個錯 → 下一篇:Event-Driven 的坑:Exactly-Once 不存在、順序不保證