結論先講
不是所有資料都需要強一致性。 付款和庫存扣減需要——你不能超賣、不能多扣錢。但通知、推薦、統計報表、搜尋索引這些,晚個幾秒甚至幾分鐘完全沒問題。微服務架構師最重要的工作之一,就是跟 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