分佈式事務:本地事務 + RPC 的“隱形炸彈”
只要系統被拆成多個微服務,“分佈式事務”就繞不過去。
很多同學只記住了@Transactional,卻忽略了一個關鍵事實:
它只對本地數據庫負責,對遠端 RPC 一無所知。
真正的坑,往往就埋在“本地事務裏嵌套 RPC 調用”這一行代碼裏。
一、問題場景:A 調 B,要保證狀態一致
典型面試題:
- 系統 A、系統 B,通過 RPC 相互調用。
- 有個業務要求:兩邊的狀態必須一致。
- A 中有一個方法加了
@Transactional:
- 先通過 RPC 調 B,B 把狀態更新為“生效中”。
- 根據 B 的返回結果,A 更新自己的本地數據為“生效中”。
問:可能出現什麼問題?
二、幾個常見的坑
1. 長時間 RPC 拖垮數據庫
- A 在開啓本地事務後,發起一個執行時間很長的 RPC。
- 事務期間,數據庫連接一直被佔用;
- 高併發時,很容易把連接池拖到見底,引發雪崩。
2. B 成功,A 回滾:兩邊狀態不一致
- B 更新成功,狀態“生效中”。
- A 在本地更新時拋異常,事務回滾,A 依然是舊狀態。
- 最終:B 生效,A 未生效,數據不一致。
3. B 實際成功,但 A 認為失敗
- 網絡抖動導致 RPC 超時:
- B 實際已經成功處理;
- A 認為調用失敗,可能回滾本地事務;
- 兩邊狀態再次不一致。
4. 超時後的“不確定性”
- 超時發生時,A 無法判斷 B 是否執行成功。
- 單靠本地事務,根本無法解決這種“不確定結果”的問題。
三、本質:@Transactional 只管本地,不管遠端
這一類問題的本質:
@Transactional只對本地數據庫負責:
- 能確保 A 自己的多條 SQL 是原子性的;
- 無法保證包含 RPC 在內的整個調用鏈是一致的。
想拿 @Transactional 去覆蓋遠端服務,是思維上的錯位。
四、主流解法一:本地消息表(最終一致)
1. 思路
- 把“本地更新”和“對外發送一條消息”放在同一個本地事務中。
- 事務提交成功後,代表:
- A 的本地狀態已更新;
- 要通知 B 的“消息”已經可靠地寫入消息表。
- 後台有一個異步任務:
- 從本地消息表裏撈出未發送或發送失敗的記錄;
- 不停重試調用 B 的接口,直到成功或超過重試上限。
2. 特點
- 獲得的是最終一致性,而不是強一致。
- 避免了本地事務直接包含長時間 RPC。
- 實現簡單,常用在中小規模系統中。
五、主流解法二:MQ 事務消息(交給消息中間件協調)
1. 思路
如果系統中已經有 MQ(如 RocketMQ):
- A 首先發送一條“半消息”到 MQ;
- 然後在本地開啓事務,更新自己的數據庫;
- 本地事務成功後,告知 MQ 提交這條消息為“正式消息”;
- B 訂閲消費這條消息,更新自己的狀態。
如果 MQ 一段時間內沒收到“提交/回滾”的反饋,它會:
- 反查 A 的本地事務狀態;
- 決定是提交還是丟棄這條消息。
2. 特點
- 把“事務協調”的複雜度交給 MQ。
- 適合對可靠性要求很高、鏈路較長的分佈式系統。
六、主流解法三:同步調用 + 業務補償(TCC 思路)
1. 思路
- 執行主業務時,同時設計一條對應的補償路徑(rollback)。
- 當任何一個環節失敗時,通過調用遠端的補償接口,把狀態拉回去。
- 不刻意追求“瞬時強一致”,而是通過補償達到“最終一致”。
2. 特點
- 非常考驗業務建模能力:
- 要為每種變更設計清晰的“撤銷邏輯”。
- 通常只在關鍵鏈路、金額類核心業務中使用。
七、異步任務失敗後的兜底
以本地消息表為例,面試官常追問:
如果異步任務也一直失敗怎麼辦?
可按以下層次回答:
- 在消息表中記錄重試次數;
- 每次失敗次數 +1;
- 超過閾值(如 3 次或 5 次):
- 標記為“死信”或“失敗狀態”;
- 觸發告警,由運維或業務方人工干預;
- 也可以設計死信隊列,專門承接多次失敗的消息。
八、沒有銀彈,只有權衡
@Transactional解決的是單服務內一致性。- 分佈式場景更多要接受:最終一致性 + 補償機制。
- 選型要在:
- 實現複雜度
- 鏈路時延
- 一致性要求
之間做平衡。
九、面試思路
- 先説明問題本質:
- 本地事務 + RPC 是不可靠的,
@Transactional不等於分佈式事務。
- 再給出主流方案:
- 本地消息表 → 最常見且易實現;
- MQ 事務消息 → 更規範但對基礎設施依賴更高。
- 然後提到補償思路:
- 對強依賴同步鏈路,可以設計補償接口落地 TCC。
- 最後回答面試官追問:
- 本地消息表的重試機制、死信隊列、人工介入策略。
本文章為轉載內容,我們尊重原作者對文章享有的著作權。如有內容錯誤或侵權問題,歡迎原作者聯繫我們進行內容更正或刪除文章。