先看單體裡的世界有多舒服

〈狀態機設計〉講過:一張訂單背後是五台狀態機(訂單、付款、退款、包裹、退貨)。在單體系統(所有 code 一個專案、一個資料庫)裡,這五台機合作起來有兩個免費的超能力:

免費超能力一:同生共死的交易。 付款成功要做四件事——付款標已付、訂單轉已付、開發票、扣庫存。單體裡把它們包進同一個資料庫交易(transaction):要嘛全成功,要嘛全部自動回滾,不存在「付款標了、訂單沒轉」的中間態。這行 @transaction.atomic 是你每天在用、卻感覺不到的奢侈品。

免費超能力二:一句 SQL 的全景查詢。 「這張訂單退到什麼程度?」——join 訂單、付款、退款三張表,一句查完。五台機在同一個資料庫,join 是白吃的。

拆成微服務後,兩個超能力同時消失

現在把它拆開:訂單服務、支付服務、物流服務,各自獨立部署、各自擁有自己的資料庫(這是微服務的鐵律,不然拆了等於沒拆)。五台狀態機分家到三個服務——然後:

交易沒了。 「付款標已付」在支付服務的 DB,「訂單轉已付」在訂單服務的 DB——跨兩個資料庫,沒有任何東西能把它們包進同一個交易。支付服務改完自己的、發一則「付款成功」事件、訂單服務收到後改自己的。這中間有時間差,而且第二步可能失敗——「付款成功但訂單還沒轉」從「不存在的狀態」變成每天都在發生的正常瞬間。這就是「最終一致性」:不是不一致,是「等一下才一致」。

join 沒了。 「退到什麼程度」要打三個服務的 API 各問一段再拼裝;或者另建一個「查詢用的彙總視圖」,訂閱各服務的事件把資料複製一份過來(這招叫 CQRS 的讀模型)——不管哪招,原本一句 SQL 的事變成一個工程。

失敗處理:從 rollback 變成 Saga

最痛的是失敗。單體裡第三步失敗,交易自動回滾前兩步,你不用寫任何補救 code。微服務裡呢?

訂單服務建了單 → 支付服務扣了款 → 庫存服務說「沒貨了」。前兩步已經提交在別人的資料庫裡,回滾不了。 唯一的辦法是反向補償:發事件叫支付服務退款、叫訂單服務標取消——每一步都要預先寫好它的「反悔動作」。這整套「一連串本地交易+每步配一個補償」的模式叫 Saga

注意這句話的重量:單體的 rollback 是資料庫送的,Saga 的每個補償都是你自己寫、自己測、自己保證冪等的業務邏輯(退款本身也可能失敗、也可能重送——見〈冪等〉系列)。而且有些動作根本沒有完美補償:信已經寄了、簡訊已經發了,你只能追加一封「更正」。

對照表:同一件事,兩個世界的成本

你要的單體微服務
多台狀態機同步變化一個 transaction事件 + 最終一致(中間態外露)
失敗回復自動 rollbackSaga 補償(自己寫、要冪等)
跨機全景查詢joinAPI 拼裝 or CQRS 讀模型
「錢跟單對不上」的兜底幾乎不需要對帳變成必需品(見〈對帳〉篇)
事件掉了不適用(沒有跨服務事件)Outbox pattern 必備

看出 pattern 了嗎?微服務那欄的每一項,都是一篇獨立的技術文章——Saga、CQRS、對帳、Outbox、冪等。這些 pattern 不是學問堆疊,它們全是在贖回單體裡免費的那兩個超能力。

所以什麼時候值得付這個價?

微服務買到的是:各服務獨立部署、獨立擴充、故障半徑隔離(支付掛了瀏覽照常)、團隊可以分頭並行。當你的痛是「組織太大、部署互相卡、單一資料庫撐不住」時,上面那張表的代價才划算。

如果痛只是「效能」——先看看單體+事件驅動(EDA)就好:HTTP 跟粗活解耦、broker 削峰,五台狀態機還是同一個 DB、交易還在。這是「先單體後拆」是預設答案的原因:把 Saga/CQRS/對帳的複雜度留到真的需要組織級擴張的那天。

相關