EP.06System Design Bootcamp

Message Queue:
用 Kafka 解耦你的系統

Producer / Consumer / Topic / Partition — 高流量系統的非同步事件驅動架構完整指南

Joseph Chen 2026 17 min read Kafka · Event-Driven · Microservices

Section 1:痛點 — 同步呼叫的三個問題

先從一個最常見的業務場景切入:用戶按下「下訂單」之後,系統需要完成哪些工作?

text
用戶按下「下訂單」
  → 扣庫存(DB 操作)
  → 建立訂單記錄(DB 操作)
  → 發送確認 Email(外部 API)
  → 通知倉庫系統(外部 API)
  → 更新推薦系統(ML 運算)
  → 產生財務報表(複雜運算)
  → 回傳「訂單成功」給用戶

如果全部同步完成,會有三個致命問題:

問題一:延遲

全部做完才回應,用戶要等待 3–5 秒才能看到「訂單成功」。 現代用戶對超過 1 秒的等待就會感到不耐,轉換率直接下降。

問題二:耦合

Email 服務掛了,整個下訂單流程就失敗。推薦系統變慢, 用戶就要等更久。一個不相關的服務,影響了核心業務流程。

問題三:擴展難

推薦系統壓力大,你想水平擴展它,但它跟訂單服務是同一個呼叫鏈, 無法獨立部署和擴展。

類比:便利商店的訂單流程

同步流程像是你在便利商店結帳,店員要當著你的面聯絡倉庫確認庫存、 幫你寄 Email 收據、更新帳本、通知配送中心 — 全部做完你才能離開。

Message Queue 讓你:下完訂單立刻收到確認收據,後面的工作(Email、倉庫通知、 推薦系統更新)全部「非同步」在背景慢慢做,你不需要等。


Section 2:Message Queue 是什麼

Message Queue 是一個「中間人」服務,讓發訊息的一方(Producer)和接收訊息的一方(Consumer) 不需要互相知道對方的存在,也不需要同時在線。

text
Producer(生產者)→ [Message Queue / Broker] → Consumer(消費者)
                     Topic(主題)
                      ├── Partition 0
                      ├── Partition 1
                      └── Partition 2

Producer(生產者)

發送訊息到 Queue 的一方。在電商系統中是「訂單服務」。 發完就走,不等待任何 Consumer 的回應。

Broker / Queue

儲存和轉發訊息的中間服務。常見的有 Kafka、RabbitMQ、AWS SQS。 負責保證訊息的持久化和可靠傳遞。

Consumer(消費者)

從 Queue 取出訊息並處理的一方。可以有多個 Consumer 訂閱同一個 Topic, 各自獨立處理自己關心的部分。

引入 Message Queue 之後,下訂單流程變成這樣

用戶按下訂單 → 訂單服務扣庫存、建立記錄 → 發布order-created事件 → 立刻回傳成功

Email 服務、倉庫服務、推薦系統、財務服務各自訂閱order-created事件,在背景依自己的速度處理。 任何一個服務掛了,都不影響下訂單本身;恢復後從 Queue 繼續消費即可。


Section 3:Kafka 核心概念

Kafka 是目前業界最主流的高吞吐量 Message Queue,由 LinkedIn 開源。 理解它的核心概念,等於理解了大多數 Message Queue 系統的共通設計邏輯。

Topic 和 Partition

Topic 是訊息的分類(例如 order-created), 每個 Topic 下面可以分成多個 Partition。Partition 是 Kafka 水平擴展的關鍵機制。

text
Topic: "order-created"
  ├── Partition 0: [msg1, msg4, msg7...]  → Consumer A
  ├── Partition 1: [msg2, msg5, msg8...]  → Consumer B
  └── Partition 2: [msg3, msg6, msg9...]  → Consumer C

並行處理

多個 Consumer 同時處理不同 Partition,吞吐量隨 Partition 數量線性擴展。 Partition 越多,理論最大並行度越高。

分區內有序

同一個 Partition 內的訊息保持嚴格的順序(FIFO)。 跨 Partition 則不保證順序,這是設計上的取捨。

Key 決定分區

Producer 可以指定 message key,相同 key 的訊息會落在同一個 Partition, 確保同一訂單的所有事件按順序處理。

Offset 和 Consumer Group

Offset 是訊息在 Partition 內的位置編號(從 0 開始)。Consumer 透過 committed offset 記錄自己處理到哪裡, 重啟後從這個位置繼續,不會遺漏也不會重複(at-least-once 語意)。

text
Partition 0: [0: "order-123", 1: "order-456", 2: "order-789"]
                                       Consumer A 已處理到這裡
                                       (committed offset: 2)

Consumer Group 是 Kafka 最強大的概念之一。同一個 Group 內的 Consumer 共同「瓜分」Partition; 不同 Group 則各自獨立消費,互不影響。

Consumer Group 的實際運作

email-service(Group 1)

  • • 訂閱 order-created
  • • Consumer A 處理 Partition 0、1
  • • Consumer B 處理 Partition 2
  • • 目的:發確認 Email

analytics-service(Group 2)

  • • 訂閱相同的 order-created
  • • 有自己的 offset,獨立消費
  • • 不影響 Group 1 的進度
  • • 目的:更新銷售統計

同一份訊息,兩個 Group 各自收到一份,各自處理。這就是「發布 / 訂閱(Pub/Sub)」模型。


Section 4:Node.js 實作(kafkajs)

kafkajs 是 Node.js 生態系中最主流的 Kafka 客戶端, API 設計清晰,對 TypeScript 友好。以下是完整的 Producer 和 Consumer 實作範例。

javascript
import { Kafka } from 'kafkajs';

const kafka = new Kafka({
  clientId: 'order-service',
  brokers: ['kafka:9092'],
});

// ── Producer(訂單服務)────────────────────────────────────────────
const producer = kafka.producer();
await producer.connect();

// 下訂單時發布事件
async function createOrder(orderData) {
  // 1. 存入資料庫(必須先做,確保資料一致性)
  const order = await db.order.create({ data: orderData });

  // 2. 發布事件到 Kafka(非阻塞,發完就回傳)
  await producer.send({
    topic: 'order-created',
    messages: [{
      key: order.id,              // 確保同一訂單的所有事件落在同一 Partition
      value: JSON.stringify({
        orderId: order.id,
        userId: order.userId,
        items: order.items,
        totalAmount: order.total,
        createdAt: new Date().toISOString(),
      }),
    }],
  });

  // 3. 立刻回傳,不等 Email / 倉庫 / 推薦系統完成
  return { success: true, orderId: order.id };
}

// ── Consumer(Email 服務)──────────────────────────────────────────
const emailConsumer = kafka.consumer({ groupId: 'email-service' });
await emailConsumer.connect();
await emailConsumer.subscribe({ topic: 'order-created', fromBeginning: false });

await emailConsumer.run({
  eachMessage: async ({ topic, partition, message }) => {
    const order = JSON.parse(message.value.toString());

    try {
      await sendOrderConfirmationEmail(order.userId, {
        orderId: order.orderId,
        items: order.items,
        total: order.totalAmount,
      });
      console.log(`已發送確認 Email,訂單 ${order.orderId}`);
    } catch (err) {
      // 錯誤處理:可以發到 Dead Letter Queue 或記錄 log
      console.error(`Email 發送失敗:${err.message}`, { orderId: order.orderId });
      throw err; // 重新拋出,讓 Kafka 知道這條訊息處理失敗(不 commit offset)
    }
  },
});

// ── Consumer(倉庫服務)────────────────────────────────────────────
const warehouseConsumer = kafka.consumer({ groupId: 'warehouse-service' });
await warehouseConsumer.connect();
await warehouseConsumer.subscribe({ topic: 'order-created', fromBeginning: false });

await warehouseConsumer.run({
  eachMessage: async ({ message }) => {
    const order = JSON.parse(message.value.toString());

    // 通知倉庫系統準備出貨
    await warehouseApi.createPickingTask({
      orderId: order.orderId,
      items: order.items,
      priority: order.totalAmount > 10000 ? 'high' : 'normal',
    });

    console.log(`已通知倉庫,訂單 ${order.orderId}`);
  },
});

錯誤處理:Dead Letter Queue(DLQ)

當訊息處理失敗時(例如 Email 服務 API 掛了),不應該無限重試阻塞正常訊息。 標準做法是設定重試次數上限,超過後送到 Dead Letter Queue,由人工或另一個服務處理。

javascript
// Dead Letter Queue 模式
const consumer = kafka.consumer({
  groupId: 'email-service',
  retry: {
    initialRetryTime: 100,     // 第一次重試等 100ms
    retries: 3,                // 最多重試 3 次
    multiplier: 2,             // 每次等待時間翻倍(100 → 200 → 400ms)
  },
});

// 如果 3 次都失敗,手動送到 DLQ
await emailConsumer.run({
  eachMessage: async ({ message }) => {
    try {
      await processMessage(message);
    } catch (err) {
      // 發到 Dead Letter Queue 供後續人工檢查
      await producer.send({
        topic: 'order-created-dlq',
        messages: [{
          key: message.key,
          value: message.value,
          headers: {
            'original-topic': 'order-created',
            'error-message': err.message,
            'failed-at': new Date().toISOString(),
          },
        }],
      });
    }
  },
});

Section 5:Kafka vs RabbitMQ — 選哪個?

Kafka 和 RabbitMQ 是最常被比較的兩個 Message Queue。它們的設計哲學根本不同, 適合的場景也截然不同。選錯工具比不用更麻煩。

KafkaRabbitMQ
設計目標高吞吐量日誌流任務佇列
訊息保留可保留數天(可重播)消費後刪除
吞吐量百萬 / 秒萬 – 十萬 / 秒
學習曲線陡峭相對平緩
適合場景事件溯源、日誌、大數據任務分發、RPC
雲端服務AWS MSK、Confluent CloudAWS SQS / SNS

選 Kafka 的時機

  • 每秒訊息量超過 10 萬
  • 需要訊息重播(分析歷史事件)
  • 事件溯源(Event Sourcing)架構
  • 多個下游服務訂閱同一事件流
  • 需要長期保留訊息供稽核

選 RabbitMQ(或 SQS)的時機

  • 簡單的任務分發(影像壓縮、Email 批次)
  • 需要複雜的路由規則(依內容分發)
  • 訊息消費後不需要保留
  • 團隊對 Kafka 不熟悉
  • 想快速在 AWS 上啟動用 SQS

Section 6:常見的事件驅動模式

引入 Message Queue 之後,系統架構通常會往事件驅動的方向演進。以下三個模式是業界最常見的應用方式。

1事件溯源(Event Sourcing)

傳統做法是儲存「當前狀態」(例如:訂單狀態 = 已出貨)。 Event Sourcing 改成儲存所有「事件」,從事件序列重建當前狀態。 這讓你可以回放任何時間點的狀態,對稽核和 Debug 非常有價值。

javascript
// 傳統做法:只儲存當前狀態
// orders 表: { id, status: 'shipped', updatedAt }
// → 無法知道「什麼時候從 pending 變成 processing」

// Event Sourcing:儲存所有事件
// order_events 表:
// { orderId, type: 'ORDER_CREATED',   timestamp, payload: {...} }
// { orderId, type: 'PAYMENT_SUCCESS', timestamp, payload: {...} }
// { orderId, type: 'ORDER_SHIPPED',   timestamp, payload: {...} }

// 從事件重建當前狀態
function rebuildOrderState(events) {
  return events.reduce((state, event) => {
    switch (event.type) {
      case 'ORDER_CREATED':
        return { ...state, status: 'pending', ...event.payload };
      case 'PAYMENT_SUCCESS':
        return { ...state, status: 'paid', paidAt: event.timestamp };
      case 'ORDER_SHIPPED':
        return { ...state, status: 'shipped', shippedAt: event.timestamp };
      default:
        return state;
    }
  }, {});
}

// 好處:可以「時光旅行」— 重播到任何時間點
const stateAtYesterday = rebuildOrderState(
  events.filter(e => e.timestamp < yesterday)
);

2CQRS(Command Query Responsibility Segregation)

讀寫分離的架構模式。「寫入操作(Command)」和「讀取操作(Query)」使用不同的資料模型和儲存, 透過 Message Queue 同步。讀取端可以針對查詢場景做專門優化。

text
寫入流程(Command):
用戶下訂單 → 訂單服務 → 寫入 Write DB → 發布 order-created 事件

同步流程(非同步):
order-created 事件 → Consumer → 更新 Read DB(針對查詢優化的結構)

讀取流程(Query):
用戶查訂單列表 → 直接讀 Read DB(可以是 Elasticsearch、Redis、Cache)
                 → 超快,因為是預先計算好的讀取優化結構

3Saga Pattern(分散式交易)

微服務架構下,一個業務操作跨越多個服務時,無法用傳統的資料庫交易(ACID)保證一致性。 Saga 透過一系列本地交易 + 補償操作(Compensating Transaction)來實現最終一致性。

javascript
// Saga:下訂單的分散式交易
// 正向流程(每步成功才到下一步)
// 下訂單 → 扣庫存 → 扣款 → 建立出貨單

// Choreography 模式(事件驅動)
// 1. 訂單服務:建立訂單 → 發布 "order-pending"
// 2. 庫存服務:收到事件 → 扣庫存 → 發布 "inventory-reserved"
// 3. 支付服務:收到事件 → 扣款 → 發布 "payment-success"
// 4. 出貨服務:收到事件 → 建立出貨單 → 發布 "order-confirmed"

// 補償流程(如果中途失敗)
// 假設扣款失敗:
// 支付服務發布 "payment-failed"
// → 庫存服務收到 → 還原庫存(補償操作)
// → 訂單服務收到 → 標記訂單失敗 → 通知用戶

// Node.js 實作(簡化版)
const sagaSteps = [
  {
    execute: () => inventoryService.reserve(orderId, items),
    compensate: () => inventoryService.release(orderId, items),
  },
  {
    execute: () => paymentService.charge(userId, amount),
    compensate: () => paymentService.refund(userId, amount),
  },
  {
    execute: () => shippingService.create(orderId),
    compensate: () => shippingService.cancel(orderId),
  },
];

async function executeSaga(steps) {
  const completed = [];
  try {
    for (const step of steps) {
      await step.execute();
      completed.push(step);
    }
  } catch (err) {
    // 倒序補償已完成的步驟
    for (const step of completed.reverse()) {
      await step.compensate().catch(console.error);
    }
    throw err;
  }
}

什麼時候不需要 Message Queue?

Message Queue 解決了很多問題,但也引入了額外的複雜度(Broker 的維運成本、最終一致性的複雜邏輯、 Debug 難度提高)。以下場景用同步呼叫就好,過早引入 Kafka 是反模式:

  • 單體應用、小流量:用 Queue 增加了運維複雜度,卻沒帶來對應的好處。
  • 強一致性需求:銀行轉帳必須「扣款成功才算轉帳成功」,不能用最終一致性。
  • 需要立即回傳結果:用戶問「我的庫存還剩幾個」,這種同步查詢不適合 Queue。

本篇涵蓋技術

KafkaMessage QueueEvent-DrivenRabbitMQSystem DesignMicroservices