面試官:微服務場景下,如何實現分布式事務來保證一致性?
為了讓系統能夠支撐更高的數據量和更復雜的業務流程,后端架構師在做架構設計的時候,通常會采用兩種核心策略:將龐大的單體應用拆分為職責單一的微服務,以及為了應對海量數據,會對單一的數據庫進行分庫分表。
這兩種策略雖然都能有效提升系統的水平擴展能力和團隊的迭代效率,但它們也帶來了一個共同的、且極為棘手的問題。當一個完整的業務操作不再局限于單一進程或單一數據庫時,我們過去所依賴的、由數據庫提供的本地事務保障(即 ACID)就自然失效了。
如何在這種全新的分布式環境下,依然保證跨越多個服務、多個數據庫的數據一致性?這便是所有后端架構師都必須面對的核心難題:分布式事務。
分布式事務是高可用系統設計中的關鍵環節,透過它能清晰的檢測出一個后端工程師的技術水平和業務能力。在技術面試中,面試官非常傾向于通過這個議題來考察你對復雜系統的理解深度。能否清晰闡述各種方案的權衡,往往決定了你能否勝任高級職位,這是一個既能充分展現亮點、也極易暴露短板的領域。
1. 核心理論:2PC、3PC 與 XA
在深入探討解決方案之前,我們必須先理清一個關鍵的前置知識:你所談論的分布式事務究竟是哪個層面的?它可能有兩個截然不同的含義。
第一種,是純粹數據庫實例間的事務,這通常由分庫分表引起。比如一個更新操作需要同時修改 user_db_0 和 user_db_1。這類事務的解決方案,往往由數據中間件(如 ShardingSphere)來提供。第二種,則是跨服務的業務層事務,這通常由微服務架構引起。比如一個下單操作,需要訂單服務、“庫存服務”和“支付服務”協同完成。這類事務的解決方案,往往由獨立的第三方中間件(如 Seata)來協調。
在面試或技術討論中,首先明確上下文是哪一類,才能給出精準的回答。而無論哪一類,它們都源于一些共同的經典理論協議。
1.1 兩階段提交 (2PC)
兩階段提交(Two-Phase Commit)是分布式事務中最廣為人知的協議。其核心思路可以概括為:引入一個協調者”=(Coordinator),由它來統一指揮所有參與者”=(Participant)的行為,要么一起提交,要么一起回滾。
它分為“準備”和“提交”兩個階段。在準備階段,協調者向所有參與者發出“準備”指令。參與者收到后,開始執行本地事務操作(如修改數據、鎖定資源),但并不提交。它們會記錄下用于后續提交的 Redo 日志和用于回滾的 Undo 日志,然后向協調者回復準備就緒(Yes)或無法準備(No)。
進入提交階段,協調者會收集所有參與者的反饋。如果所有參與者都回復Yes,協調者就向它們發出“提交”(Commit)指令,參與者收到后執行本地事務的 Commit。反之,如果任何一個參與者回復No,或者有參與者超時未響應,協調者就向所有參與者發出“回滾”(Rollback)指令,參與者根據 Undo 日志回滾所有操作。

2PC 協議聽起來很完美,但它在工程實踐中存在幾個致命缺陷。首先是同步阻塞,這是 2PC 最大的問題。在兩個階段中,所有參與者都在“等待”協調者的指令。尤其是在階段一回復“Yes”之后,參與者必須鎖定事務資源,直到階段二的最終指令到來。如果網絡延遲高,或者某個參與者處理緩慢,所有節點都會被阻塞。更嚴重的是,這種阻塞會“傳染”:若節點 A 鎖定了資源,節點 B 在等待 A,此時節點 C 恰好要訪問 B,那么 C 也會連帶著被阻塞。
其次,協調者構成了單點故障。協調者是整個系統的大腦。如果協調者在階段二發出最終指令前宕機,所有參與者將永遠卡在準備階段,無法得知該提交還是回滾,導致資源被無限期鎖定。
最后,它的容錯策略非常保守。在準備階段,任何一個節點的失敗(或網絡超時)都會導致協調者觸發全局回滾。在網絡不穩定的情況下,這可能會導致大量本可以成功的事務被“誤殺”,造成不必要的回滾。
那么,如果故障發生在階段二呢?這里有兩個值得深思的場景。第一個場景:協調者發出了 Commit,但某個參與者沒收到。 這種情況下,協調者會不斷重試,直到該參與者確認收到 Commit 指令為止。這是為了保證所有節點最終都能達成一致。

第二個場景:參與者收到了 Commit,但在執行本地提交前宕機了。 此時,當這個參與者節點恢復后,它會去檢查自己的本地事務日志。它會發現自己已經完成了“準備”階段,并且也收到了“Commit”指令,于是它會利用之前記錄的 Redo 日志,自動完成事務的提交,以追上大部隊的狀態。

1.2 三階段提交 (3PC) 與 XA 規范
為了緩解 2PC 的一些問題,特別是“準備階段白忙活”(一個節點準備好了,結果另一個節點失敗導致全局回滾)和“同步阻塞”過久,學術界提出了三階段提交(3PC)。

它在 2PC 基礎上增加了一個 CanCommit 階段。協調者先問一圈:“各位,你們的資源和狀態是否允許執行此事務?”(純檢查,不鎖定資源)。如果大家都回復“可以”,協調者才發起 PreCommit 階段(類似 2PC 的準備),參與者此時才真正開始執行事務(鎖定資源、寫 Redo/Undo)。最后進入 DoCommit 階段,發起提交或中止指令。
3PC 試圖通過 CanCommit 階段提前發現無法執行的節點,減少“白忙活”的概率。但它也帶來了更多的網絡往返,使得整個流程更長、更復雜。在工程實踐中,人們發現 2PC 已經能解決大部分問題,而 3PC 帶來的這點收益與其增加的復雜度相比,性價比很低,因此并未被廣泛采用。
如果說 2PC/3PC 是學術理論,那么 XA 規范 就是將 2PC 理論具象化后的工程標準。XA 規范是由 X/Open 組織提出的分布式事務處理(DTP)模型,它并非一種協議,而是一套接口標準。它精確地定義了事務管理器(TM,即協調者)和資源管理器(RM,即實現了 XA 接口的數據庫等參與者)之間的通信接口。只要數據庫遵循了 XA 規范,理論上它就能被一個 XA 事務管理器所納管,參與到全局的 2PC 事務中。
2. 強一致性與最終一致性
2.1 XA 與 ACID 的一致性爭議
這是一個在面試中經常被深入探討的爭議點:XA 究竟滿足 ACID 嗎?我個人的傾向是,XA 并不完全滿足嚴格的 ACID。
核心在于C(Consistency,一致性)。嚴格的一致性要求數據在任何時刻都處于一致狀態。但 XA(即 2PC)在執行過程中,存在一個明顯的不一致窗口。當協調者在階段二發出 Commit 指令后,如果某個參與者 A 提交成功,而參與者 B 因為網絡抖動尚未收到 Commit 指令(或正在重試),此時系統的數據就處于“A 提交了,B 沒提交”的中間態。在這個不一致窗口內,如果有外部讀取,就會看到不一致的數據。
因此,我們只能說,XA 是所有分布式方案中,最接近嚴格 ACID 的一種,但它為了追求這種強一致,付出了高昂的性能代價(同步阻塞)。
2.2 最終一致性
正是因為 XA 方案的高昂代價,導致低吞吐量、出現同步阻塞,在追求高可用、高性能的現代互聯網架構中,它幾乎不被采用。
架構師們必須做出妥協:放棄強一致性(即時一致性),轉而擁抱最終一致性。
最終一致性允許系統在執行過程中存在短暫的數據不一致(即中間態),但它承諾并有機制保障,在經歷了一系列(可能出錯和重試的)步驟后,所有相關節點的數據最終會達到一致的狀態。
在面試中,當你闡述完 XA 的局限性后,自然地引出最終一致性是業界的普遍選擇,并準備深入探討 TCC、SAGA 等方案,這通常是面試官所期待的。
在實際工作中,你也應該對自己的系統有清晰的認知。比如,如果使用了分庫分表,我們是否允許跨庫事務?如果允許,用的是什么方案?是 XA 還是中間件的延遲事務?在微服務層面,我們使用的是哪種分布式事務方案?TCC、SAGA 還是 Seata AT?最關鍵的是:當中間步驟出錯時,我們的容錯和補償機制是什么?
面試官也經常會從這些實際問題出發,例如:“單體拆分成微服務后,原本的本地事務是如何改造的?”“你們的多個服務是共享數據庫嗎?如果不是,如何保證數據一致性?”,“一個
DELETE語句經過分片后要刪除多個表的數據,你如何保證原子性?”這些問題的本質,都是在考察你對“最終一致性”方案的理解和實戰經驗。
3. 主流方案:TCC、SAGA 與 AT
基于最終一致性的思想,業界在微服務架構中演進出了三種主流的實現方案。
3.1 TCC
TCC即Try-Confirm-Cancel,是最終一致性的一種主流實現,它巧妙地將 2PC 的思想從數據庫資源層上移到了業務邏輯層。它的核心在于 Try(嘗試執行業務,檢查并預留資源)、Confirm(確認執行業務,真正使用 Try 階段預留的資源) 和 Cancel(取消執行業務,釋放 Try 階段預留的資源)。
TCC 其實可以看作是應用層實現的兩階段提交,簡而言之就是在業務層完成兩次動作,才算事務生效。應用層的兩階段提交(TCC )的精髓在于,Try、Confirm、Cancel 這三個操作,各自都是一個完整的、獨立的本地事務,可以立即提交。我們用一個更詳細的“下單鎖庫存”案例(變更自原文)來理解:假設用戶購買 item_id=888 的商品 1 件。
Try階段成功:
- 訂單服務會
INSERT INTO orders (item_id, status) VALUES (888, 'PENDING')并提交本地事務; - 庫存服務會
UPDATE inventory SET available_stock = available_stock - 1, locked_stock = locked_stock + 1 WHERE item_id = 888 AND available_stock >= 1并提交本地事務。此時,資源已被預留。

如果所有 Try 均成功,TCC 框架開始調用 Confirm 階段。
- 訂單服務
UPDATE orders SET status = 'CONFIRMED' WHERE ... - 庫存服務
UPDATE inventory SET locked_stock = locked_stock - 1 WHERE item_id = 888
各自提交本地事務,此時資源被真正使用。
Try階段失敗:
如果 Try 階段中,庫存服務因并發導致 available_stock 不足而失敗,TCC 框架開始調用 Cancel 階段。
- 訂單服務
UPDATE orders SET status = 'CANCELLED' WHERE ... - 庫存服務由于之前檢查數量不足沒有執行數量的扣減,這里無需執行cancel操作。如果之前 Try 已成功。
UPDATE inventory SET available_stock = available_stock + 1, locked_stock = locked_stock - 1 WHERE item_id = 888。各自提交本地事務,釋放預留的資源。

其實可以認為Cancel的操作就是一個前面Try操作的逆過程
TCC 方案雖然清晰,但它的難點藏在容錯里。Try 階段出錯還好,業務本就無法使用(如訂單是 PENDING),直接 Cancel 即可。最嚴重的是 Confirm 或 Cancel 階段出錯。想象一下,如果 Confirm 階段,訂單服務成功了(CONFIRMED),但庫存服務的 UPDATE locked_stock 調用超時或失敗了,怎么辦?此時訂單是 CONFIRMED,庫存卻是 LOCKED,數據嚴重不一致。
最容易想到的答案是:重試。TCC 框架必須不斷地、冪等地(這點至關重要,Confirm/Cancel 必須可重復執行而不產生副作用)重試失敗的 Confirm(或 Cancel)操作,直到它成功為止。那如果重試了幾十次還是失敗呢(比如庫存服務有 Bug,或者徹底宕機)?這時候,光靠框架的無限重試就不行了,必須有兜底保障機制(這才是面試的亮點)。
最基礎的方案是監控 + 告警 + 人工介入。當重試連續失敗(例如 10 次)后,框架不再重試,而是發出嚴重告警,由 SRE 或開發人員手動介入處理(比如手動修復數據、重啟服務)。
更進一步的方案,是異步校準修復(離線)。建立一個離線的數據比對程序(如定時 Job),T+1 地掃描數據。例如,掃描所有“CONFIRMED”狀態超過 5 分鐘的訂單,去和庫存系統比對,一旦發現訂單已確認、但庫存還處于“LOCKED”狀態的數據,就立即執行修復。

在某些高時效性場景下,還可以采用實時讀取修復(在線)。比如在用戶查詢訂單詳情時,聚合服務需要同時調用訂單庫和庫存庫。當它在讀取時發現數據不一致(訂單 'CONFIRMED',庫存 'LOCKED'),它可以“機智”地返回給用戶一個“處理中”的友好提示,并異步觸發一個修復程序去修正這個臟數據。

3.2 SAGA
SAGA 是另一種截然不同的思路。TCC 是先預留,再確認,而 SAGA 則是立即執行,失敗后再補償。
一個 SAGA 事務由一系列子事務(Local Transaction)構成。每個子事務都會立即提交。如果某個子事務失敗了,SAGA 會啟動補償事務(Compensation Transaction),按相反的順序,把前面已經成功提交的子事務撤銷掉。
這里必須厘清補償和回滾的核心區別:回滾是數據庫層面的,利用 Undo Log,事務并未提交;而補償是業務層面的反向操作,原事務已經提交,補償是一個新的本地事務。
我們用跨行轉賬的例子來理解:在正向流程中,事務1(A 銀行) UPDATE account SET balance = balance - 100 WHERE id = 'A',立即提交。緊接著,事務2(B 銀行) UPDATE account SET balance = balance + 100 WHERE id = 'B',但執行失敗(例如 B 賬戶被凍結)。此時,SAGA 啟動反向補償,執行補償事務1(A 銀行):UPDATE account SET balance = balance + 100 WHERE id = 'A' 并立即提交,即 A 銀行加回100 塊。

SAGA 模式特別適合那些業務流程長、涉及多個服務的場景。它還有一個 TCC 不具備的亮點:支持并發執行。比如一個復雜的“旅行預訂”SAGA,它可以并發地執行“預訂機票”、“預訂酒店”、“預訂租車”三個子事務,因為這三者之間可能并無依賴,并發執行可以大大縮短整個 SAGA 事務的總體耗時。

SAGA 的容錯難點和 TCC 一樣:如果補償事務失敗了怎么辦?(比如 A 銀行的補償加錢操作失敗了)。答案也一樣:重試、重試、再重試。SAGA 框架必須保證補償操作的最終成功。如果實在不行(比如A銀行系統持續不可用),只能告警 + 人工介入或觸發前面提到的自動修復機制。
3.3 AT
TCC 和 SAGA 都需要我們手寫大量的業務邏輯(Confirm/Cancel 或補償事務),對業務的侵入性很強。因此,像 Seata 這樣的中間件,提供了一種更便捷的模式——AT(Automatic Transaction)。
你可以把 AT 模式理解為自動化的 SAGA。它的核心原理是利用了數據庫的 undo_log。在你的本地事務(比如 UPDATE)執行前,中間件會先解析 SQL,查出before的數據快照。然后,你的本地事務正常執行。在本地事務提交前,中間件將before快照存為一個 undo_log 記錄(寫在同一數據庫中),undo_log 和業務數據的修改是在同一個本地事務里提交的。如果全局事務需要回滾,中間件就會讀取 undo_log,自動生成反向 SQL(比如把 UPDATE 再 UPDATE 回去),幫你完成補償。
這種模式對業務的侵入性最小,你幾乎不用改代碼。但它依賴于數據庫(需要
undo_log表)和 SQL 解析(不支持某些復雜 SQL),靈活性自然也就差一些。
3.4 延遲事務
TCC、SAGA、AT 更多是在微服務層發力。那如果我們只是單純的分庫分表,有沒有更輕量、更高效的方案?
答案當然有。首先,最簡單直接的方案,就是在公司規范里禁止跨庫事務。通過合理的分片鍵設計(比如按 user_id 分片),確保 99% 的事務都在單一分片內完成。如果業務非要跨庫,那就拆分成多個本地事務,在業務層自己保證最終一致性。
但如果真的(非要)有跨庫事務,又不想上 TCC 那么重,分庫分表中間件(如 ShardingSphere)提供了一種非常巧妙的方案——延遲事務(Delayed Transaction)。這也是你在面試時候的可以展現給面試官的終極亮點方案。
3.4.1 延遲事務原理
這個方案要解決一個核心痛點:當應用層執行 Begin(開啟事務)時,中間件根本無法預測你接下來的 SQL 會落到哪個分庫上。

比如,一個 UPDATE 可能根據參數 user_id=50 路由到 db_0,也可能根據 user_id=51 路由到 db_2。

中間件只有兩個選擇。一種是資源消耗大的方案:執行 Begin 時,在所有分庫上都開啟一個真實事務。這太浪費資源了,可能你最后只用到了 db_2,卻在 db_0 和 db_1 上白白占用了連接和事務資源。

所以,按需開啟事務時更合理的,它避免在用不上的數據庫上開啟事務,所以它的應用也更加廣泛

另一種是優化方案,即延遲事務:應用層執行 Begin,中間件什么也不做,只是在內存中標記一下事務已開始。當應用層執行第一條 SQL(比如 UPDATE ... WHERE user_id = 50),中間件解析出它需要路由到 db_0。此時,中間件檢查 db_0 上是否已開啟事務,發現沒有,于是在 db_0 上真正開啟一個本地事務,并執行 SQL。接著,應用層又執行第二條 SQL(UPDATE ... WHERE user_id = 51),路由到 db_2。中間件檢查 db_2,也在 db_2 上開啟一個本地事務,并執行 SQL。如果應用層執行第三條 SQL(UPDATE ... WHERE user_id = 50 ...),路由到 db_0,中間件檢查 db_0,發現事務已開啟,于是復用該事務執行 SQL。最后,當應用層執行 Commit 時,中間件才將 db_0 和 db_2 上所有“延遲開啟”的事務,統一執行 Commit。
3.4.2 容錯機制:自動故障處理
這個“按需開啟”的方案是不是非常高效?但是,它依然沒有逃過那個根本問題:如果 Commit 的時候,db_0 提交成功了,但 db_2 提交失敗(或超時)了,怎么辦?

我們又回到了原點:重試。中間件會立刻重試失敗的 Commit。
如果重試都失敗了呢?這就引出了我實戰中用過的更完善的高階方案:自動故障處理機制。
當中間件連續重試 Commit 失敗后(例如 3 次),它不再只是“告警”,而是異步發送一條消息(比如到 Kafka 或 RocketMQ)。這條消息必須攜帶足夠的上下文,例如全局事務ID、目標庫、目標表、主鍵、失敗階段等。

然后,我們有一個獨立的自動修復服務來消費這條消息。這個服務會根據業務邏輯,來決定是正向修復(用 db_0 已提交的數據去強行修復 db_2 的數據)還是逆向補償(把 db_0 的數據給回滾了)。這個修復邏輯通常是和業務強相關的,需要業務方來定義和接入。
面試在回答這個方案的時候,還可以輔以一個實際工作案例。比如,在我之前的一個高可用項目中,我們引入了這個機制,讓業務方可以自定義修復邏輯。最終,系統的可用性從P80提升到了P99,大大減少了需要人工介入處理的線上問題。
4. 小節
回顧我們的整個分布式事務方案,我們首先從理論(2PC/3PC/XA)出發,然后以最終一致性為前提,再到業界主流的分布式事務具體實踐。在微服務場景下,我們有 TCC(業務侵入強,控制力高)、SAGA(流程編排,易于理解)和 AT(自動補償,侵入性低)。在分庫分表場景,我們有延遲事務這種輕量級的優選方案。你會發現,所有方案最后都指向了一個共同的核心難點——容錯。
一個高階架構師的價值,不在于知道 TCC 和 SAGA 的區別,而在于你如何設計那套“重試 + 冪等保障 + 告警 + 自動/手動修復”的完整保障機制。這才是保證系統在復雜的線上環境中最終還能保持穩定的核心。
最后,技術的世界里,很多時候沒有絕對的對錯,只有不同場景下的偏好和權衡。具體方案一定是視場景而定,不同場景有不同的方案選擇。在面試中,我們也要盡可能全面的展現我們的思維,我們的目標是設計和構建出真正魯棒的系統,而不是執著于某個具體方案的好與壞。




























