結論先講

在做單體的時候,你完全不會覺得需要 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:選哪個

維度KafkaRabbitMQ
核心模型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 不存在、順序不保證