結論先講

不是所有資料都需要強一致性。 付款和庫存扣減需要——你不能超賣、不能多扣錢。但通知、推薦、統計報表、搜尋索引這些,晚個幾秒甚至幾分鐘完全沒問題。微服務架構師最重要的工作之一,就是跟 PM 一起把每個場景歸類到「必須強一致」或「可以最終一致」。


強一致 vs 最終一致

強一致性(Strong Consistency):
  寫入完成的瞬間,所有讀取都拿到最新值。
  就像單體的 DB transaction——commit 之後就是最新的。

最終一致性(Eventual Consistency):
  寫入完成後,過「一段時間」所有讀取才會拿到最新值。
  這段時間可能是毫秒、可能是秒、可能是分鐘。

日常生活中的最終一致

你其實天天在接受最終一致:

  • 銀行轉帳:跨行轉帳 1-3 個工作天才到帳——你不會因為「ATM 顯示餘額還沒減少」就覺得轉帳沒成功
  • 社群媒體:你發了一則貼文,你的朋友可能 1-2 秒後才看到——你不會因為「延遲 1 秒」就投訴
  • 電商庫存:商品頁顯示「剩 5 件」,但你加入購物車時可能已經被買走了——這就是讀到了過時的資料

用戶在乎的不是「資料是不是即時一致」,是「最終結果有沒有錯」。


微服務裡哪些場景用哪種

必須強一致的場景

場景為什麼做法
扣款 / 退款多扣一毛錢都是法律問題同一個服務內的 DB transaction
庫存扣減超賣 = 需要人工處理退款 + 道歉同一個服務內的 DB transaction + 樂觀鎖
帳戶餘額餘額不對 = 客訴爆炸同一個服務內的 DB transaction
權限變更停權後還能操作 = 安全漏洞同步 API call + cache invalidation

關鍵字:「同一個服務內」。 強一致性只能在單一 DB 的 transaction 裡保證。跨服務的強一致性,用 第 46 篇 的 Saga Pattern 趨近——但本質上還是最終一致。

可以最終一致的場景

場景可以延遲多久為什麼可以
通知(Email / 推播)秒 ~ 分鐘晚幾秒收到通知,用戶不在意
搜尋索引秒 ~ 分鐘新商品晚 30 秒出現在搜尋結果,沒人發現
推薦系統分鐘 ~ 小時推薦不即時更新不影響體驗
統計報表分鐘 ~ 小時報表本來就不是即時的
用戶 Profile 快取改了名字,別人晚幾秒看到新名字
活動計數(按讚數、瀏覽數)少顯示 1 個讚不會有人投訴

最終一致的「時間窗口」怎麼控制

事件驅動的延遲來源

寫入 DB(1ms)
  → 發送 Event 到 Kafka(5ms)
    → Consumer 收到(10-100ms,取決於 batch size)
      → Consumer 處理 + 寫入自己的 DB(5ms)

總延遲: ~20-110ms(正常情況)

正常情況下,最終一致的延遲是毫秒到百毫秒等級。但如果:

  • Kafka 積壓 → 延遲可能到秒級
  • Consumer 掛了重啟 → 延遲可能到分鐘級
  • Event 進了 Dead Letter Queue → 需要人工處理

監控最終一致的延遲

# 監控 Kafka consumer lag
kafka_consumergroup_lag_sum{consumergroup="order-events-consumer"}
 
# 如果 lag 持續增加 → consumer 處理速度跟不上
# 告警閾值:lag > 1000 且持續 5 分鐘

CQRS:讀寫分離的進階做法

CQRS(Command Query Responsibility Segregation)把「寫入模型」和「讀取模型」完全分開。

傳統做法:讀寫同一個 DB

用戶下單 → 寫入 OrderDB
用戶查訂單 → 讀取 OrderDB
管理員查報表 → 讀取 OrderDB(複雜 JOIN + 聚合 → 很慢)

CQRS:寫入和讀取分開

寫入端(Command):
  用戶下單 → 寫入 OrderDB(正規化、事務完整)
              → 發送 Event [order.created]

讀取端(Query):
  [order.created] → 更新 ReadDB(反正規化、查詢優化)
  用戶查訂單 → 讀取 ReadDB(快)
  管理員查報表 → 讀取 ReadDB(預先聚合好、更快)

什麼時候用 CQRS

場景用不用 CQRS
讀寫比 10:1 以上值得考慮
讀取需要跨多個服務的資料值得考慮
報表查詢很複雜(多表 JOIN)值得考慮
讀寫比 1:1不需要
團隊 < 5 人不需要(複雜度太高)
CRUD 應用(後台管理)不需要

CQRS 的代價是複雜度。 你多了一個 ReadDB 要維護、多了 Event Consumer 要處理、讀取端的資料是最終一致的——用戶剛下完單,重新整理頁面可能還看不到自己的訂單。

解決「自己的寫入看不到」

// 方案 1:寫入後直接回傳結果,不查 ReadDB
app.post('/orders', async (req, res) => {
  const order = await orderService.create(req.body);
  // 直接回傳剛寫入的資料,不從 ReadDB 查
  res.json(order);
});
 
// 方案 2:前端用樂觀更新
// 按下送出 → 前端立刻顯示成功 → 背景同步
// React Query 的 optimistic update 就是這個概念
 
// 方案 3:Read-your-writes consistency
// 帶上 write timestamp,ReadDB 還沒更新到該時間就去查 WriteDB

實戰案例:電商訂單系統

用戶操作          一致性需求        做法
─────────────────────────────────────────────
下單扣庫存        強一致           庫存服務內 DB transaction + 樂觀鎖
下單扣款          強一致           付款服務內 DB transaction
更新訂單狀態      最終一致(秒)    Saga event → 訂單服務更新
發送訂單通知      最終一致(秒)    Event → 通知服務
更新搜尋索引      最終一致(分鐘)   Event → ES indexer
更新推薦模型      最終一致(小時)   Batch job
更新銷售報表      最終一致(小時)   Batch job / Streaming

注意「強一致」都在單一服務內——因為只有單一 DB 的 transaction 才能真正保證 ACID。跨服務的部分,Saga 負責最終一致。


跟 PM 怎麼溝通

工程師常犯的錯:什麼都要強一致。PM 常犯的錯:什麼都要即時。

開會時的對話模板

工程師: 「這個功能,訂單建立後多久用戶能在訂單列表看到?」
PM:     「立刻啊,不然用戶以為沒成功。」
工程師: 「下單的確認頁面可以立刻看到(我們直接回傳結果)。
         但如果用戶回到訂單列表頁重新整理,可能 1-2 秒
         才出現。可以接受嗎?」
PM:     「1-2 秒 OK,但不能超過 5 秒。」
工程師: 「好,那 SLO 設 3 秒。」

把「最終一致」翻譯成「延遲幾秒」,PM 就能做決策了。 不要跟 PM 講 CAP theorem。

一致性 Decision Matrix

場景PM 在意什麼技術翻譯做法
付款結果不能多收錢強一致單服務 transaction
訂單確認頁立刻看到寫後讀一致直接回傳寫入結果
訂單列表幾秒可以最終一致(SLO 3s)Event + ReadDB
推薦商品不即時沒差最終一致(無 SLO)Batch / Streaming

常見的坑

1. 過度追求強一致性

「為了保證一致性,我們把 5 個服務的寫入都用同步 API call 串起來」

→ 延遲: 5 個服務的延遲加總(50ms × 5 = 250ms baseline)
→ 可用性: 5 個服務任一個掛了就全掛(0.99^5 = 0.95)
→ 耦合: 改一個服務要測全部

這就是 [[micro-service/29-real-world-antipatterns|第 29 篇]] 說的「分散式單體」。

2. 忽略最終一致的用戶體驗

用戶下單 → 成功
用戶立刻去訂單列表 → 看不到訂單 → 以為失敗 → 再下一單

→ 兩筆訂單、兩次扣款、客訴電話

解法:
- 下單成功後直接導到訂單詳情頁(不經過列表頁的 ReadDB)
- 或在列表頁加 loading 提示:「訂單處理中,請稍候」

3. 沒有監控一致性延遲

最終一致的「最終」如果變成 10 分鐘,用戶會抱怨。你需要監控:

  • Kafka consumer lag
  • Event 從發送到處理的 end-to-end 延遲
  • ReadDB 跟 WriteDB 的資料差異筆數

設告警:延遲 > SLO(例如 3 秒)持續 5 分鐘 → P1 告警。


整理:微服務一致性的完整工具箱

工具保證的一致性適用場景
單一 DB Transaction強一致同一服務內的多表操作
Saga(Choreography)最終一致2-3 個服務的簡單流程
Saga(Orchestration)最終一致4+ 個服務的複雜流程
Event + Consumer最終一致通知、索引、報表
CQRS最終一致(讀端)高讀寫比、複雜查詢
2PC強一致(但代價大)盡量不用

下一篇

CD — 資料一致性搞定了,接下來面對另一個痛點:5 個服務怎麼部署?全部一起部署還是各自部署?一個服務改了 API 合約,怎麼確保不會打爆其他服務?


本系列文章

完整 68 篇目錄見 系列首頁

← 上一篇:資料一致性(一):Saga Pattern 取代分散式 Transaction → 下一篇:CD:每個 Service 獨立 Pipeline