分布式事務實戰方案匯總
- 1. 最終一致性
- 1.1 本地事務表 + 輪詢補償
- 互動流程
- 場景:重構業務新老系統雙寫庫同步
-
- 場景:第三方認證核驗
-
- 1.2 本地事務表 + 事務訊息
-
- 1.3 TCC(Try-Commit-Cancel)
- 互動流程
- 場景:積分商品兌換
-
- 場景:廣告任務結算
-
- 場景:運營活動抽獎
-
- 2. 弱一致性
- 2.1 最大努力通知 + 訊息重試控制
- 場景:資料變更同步下游業務方
-
- 場景:資料變更廣播下游業務方
-
- 2.2 戰略放棄 + 報警 + 人工修復
-
- 3. 總結
- 3.1 分布式角色
- 3.2 技術保證
- 3.2 強弱一致選擇
- 3.3 冪等&防重
- 3.4 盡早干預&補償一致
- 5. 參考
1. 最終一致性
1.1 本地事務表 + 輪詢補償
互動流程

- ① commit DB事務提交階段 本地客戶端向DB進行事務提交,此時需要將業務資料和記錄訊息事務狀態的資訊表同時實作本地事務,此時標記訊息事務狀態為UN_SEND未發送或未完成狀態,此時MQ未發送
- ② ack DB確認階段 回傳DB事務提交成功或失敗狀態
- ③ commit MQ事務提交階段 客戶端發起MQ發送請求
- ④ update 本地事務表更新階段 根據MQ發送結果進行本地訊息事務表狀態更新,成功則更新為SEND發送成功或發送完畢
- ⑤ MQ補償 本地訊息事務表定時輪詢 對未發送成功訊息事務進行補償發送,實作分布式事務的最終一致
場景:重構業務新老系統雙寫庫同步

專案背景
這是一個重構系統新老系統同時服役切量遷移的業務場景,老系統正在線上服役為各業務方提供介面查詢功能,新系統重構完成后需要對接接入,呼叫流量要陸續從老系統切換到新系統,最終老系統迭代下線,
分布式事務
需要解決的分布式事務問題就是,雙系統的資料是異構、分散的,線上不可停量,需要陸續切換完成,因此需要事先將老庫存量資料洗入新庫,此程序中增量資料寫入是存在的,但是最終新老庫是相對一致和統一的,該場景需要解決的是資料雙庫的雙寫問題,
設計方案
| 系統 | 寫操作 | 讀操作 | 呼叫方 |
|---|
| 老系統 | 異步寫(MQ) | 讀(冗余查) | 歷史存量渠道 |
| 新系統 | 同步寫 | 讀 | 新業務渠道/歷史存量切換渠道 |
場景Q&A
- Q:如何保證雙庫雙寫?
A: 同步寫辛庫,MQ異步寫老庫
- 本地事務 + 訊息事務表
業務資料持久化開啟資料庫本地事務,該事務中記錄業務資料和同步狀態資訊 - 確保本地事務一定成功,不保證異步MQ事務
資料庫事務成功后再發送寫老庫的MQ,保證本地事務一定完成才會觸發MQ發送,這樣確保本地事務一定成功,MQ可能成功也可能失敗 - 重試MQ事務狀態,最終一致
如果MQ事務失敗,通過定時任務輪詢進行重發驅動,最終一致
- Q:異步MQ寫,延遲問題如何解決?
A: 增加冗余查詢
增加冗余對另一庫的冗余查詢進行Double Check,由于呼叫方幾乎不可能同時使用新老系統做業務,因此延遲時間取決于MQ異步消費寫的速度,如果場景比較復雜要確保絕對一致可以增加該處理方式
- Q:雙寫程序中多個MQ如何保證順序、防重等問題?
A: 業務時間 + 業務ID
- (1)同一個
業務ID代表一個同一筆業務,可以依此進行業務防重處理
| 訊息業務ID | 業務時間 | 消費時間 | 處理邏輯 |
|---|
| ID | T | T | 執行 |
| ID | T | T+1 | 防重不處理 |
- (2)同一個
業務ID的基礎上增加業務時間,可以依此保證業務資料的實時重繪
業務時間、消費時間同序
| 訊息業務ID | 業務時間 | 消費時間 | 處理邏輯 |
|---|
| ID | T | T | 執行 |
| ID | T+1 | T+1 | 執行(覆寫) |
業務時間、消費時間亂序
| 訊息業務ID | 業務時間 | 消費時間 | 處理邏輯 |
|---|
| ID | T | T+1 | 拋棄(業務時間較早) |
| ID | T+1 | T | 執行 |
- (3)不同
業務ID的基礎上增加業務時間,可以依此保證不同業務資料的按照業務時間重繪
| 訊息業務ID | 業務時間 | 消費時間 | 處理邏輯 |
|---|
| ID | T+1 | T+1 | 執行(業務時間較新) |
| ID+X | T | T | 拋棄(業務時間較早) |
場景:第三方認證核驗

專案背景
這是一個認證系統以來外部核驗系統進行用戶身份鑒權的場景,即認證系統記錄認證發起記錄,并請求到外部的核驗系統發起一筆核驗請求,用戶在核驗系統確認后核驗結果回傳到認證系統確認用戶的真實資料狀態,
分布式事務
該流程中認證系統是一個本地系統,存放用戶發起的認證流水資訊和核驗狀態,依賴外部核驗系統回傳該筆認證記錄的核驗狀態,由于核驗程序是異步的,用戶可以選擇任意時間完成或者永遠不完成,需要保證每次認證流程只有一筆業務發起,而且需要根據業務時間進行核驗流程的超時進行強制取消或者補償查詢對齊核驗狀態等,需要解決的分布式事務是認證流水、核驗結果的一致性,
設計方案
| 資料 | 系統 | 事務 | 正向事務 | 逆向事務 |
|---|
| 認證流水資料 | 本地系統 | 本地事務 | 本地事務2PC保證 | 定時任務,超時關閉流程 |
| 核驗狀態資料 | 外部系統 | 分布式事務 | 聯同本地事務提交 / 定時任務驅動補償提交 | 外部系統控制 |
場景Q&A
- Q:本地事務認證流水成功了,外部核驗系統提交失敗了怎么辦?
A:通過定時任務補償觸發二次提交,只要外部事物提交一直處于未成功,便一直會被重試提交,直到成功
- Q:外部核驗系統事務完成了,本地事務認證流水提前被作廢了怎么辦?
A:以本地事務認證流水的結果為準,本地事務是通過定時任務進行補充提交+外部事務狀態核驗查詢的,即時在臨界點外部事務完成了,但是超過了業務處理時間已經關閉,不會再補充修改,這也是根據業務場景做的取舍,用戶可以再次發起新流程進行核驗
1.2 本地事務表 + 事務訊息
互動流程

- ① prepare 準備階段 本地客戶端向DB、MQ發送prepare請求
- ② ack 準備確認階段 DB、MQ作為事務參與者回傳本地客戶端ack確認
- ③ commit/rollback 提交/回滾階段 根據事務參與者在準備確認階段回傳結果進行事務提交或回滾,此時一旦有事務參與者回傳例外或超時未回傳則進行回滾提交
- ④ callback 補償回呼階段 當事務超時提交時,則由MQ進行回呼查詢資料庫本地事務情況
- ⑤ commit/rollback 提交/回滾階段 根據補償回呼階段進行事務提交和回滾,實作分布式事務的最終一致性
場景:分庫分表路由欄位系結

專案背景
該業務是在分庫分表場景下,需要一個切分鍵欄位進行資料分片路由,一般我們ToC業務的話會使用userId這樣的欄位進行切分,然而水平切分資料帶來了資料庫讀寫性能的同時也存在一個問題,那就是查詢必須攜帶切分鍵才可以完成,因為要依賴它進行資料路由查詢,比如在以userId進行資料路由切分時,如果想按照用戶的身份證、姓名等進行匹配查詢是無法實作的,因為我們事先是不知道userId、身份證、姓名的等值匹配關系,一般而言,我們可以通過HBase + ES的方案進行解決,即監聽不同庫的binlog日志,將其按照時間進行排序切分匯入HBase表中,加入要檢索的索引到ES中解決分庫分表下資料切片產生的聚合問題,
分布式事務
基于以上場景,除了通過HBase+ES實作之外,還可以通過切分鍵與非切分鍵進行資料系結解決,但是由于切分鍵與非切分鍵的路由一般不一致,資料會分散到不同庫,因此無法做本地事務,這是我們需要解決的分布式事務問題,
設計方案
| 欄位 | 處理方式 | 路由鍵 | 存盤格式 |
|---|
| 切分鍵 | DB同步寫,按照切分鍵路由 | 切分鍵 | <切分鍵,非切分鍵A,非切分鍵B,非切分鍵C> |
| 非切分鍵 | MQ異步寫,按照非切分鍵路由冗余存盤,保存與切分鍵資料關系 | 非切分鍵 | <非切分鍵A,切分鍵> <非切分鍵B,切分鍵> <非切分鍵C,切分鍵> |
場景Q&A
- Q:異步MQ寫有延遲,查不到切分鍵如何處理?
A:事務處理開始階段通過Redis記錄事務開啟的分布式鎖標識,當其他存在沖突的同業務在該事務的處理程序有查詢時,通過判斷分布式鎖標識是否存在來判斷事務的開啟,若存在則異步等待并發起間隔查詢直到事務超時,若事務超時周期內反復查詢到則回傳,否則根據事務處理后結果回傳
- Q:本地事務與MQ事務是如何協作的?
A:相當于兩個2PC事務協作,
- 1.一階段DB、MQ同時prepare(并行)
- 2.二階段DB先commit,成功后MQ再commit(串行)
| 流程 | 階段 | 操作 | Ack反饋 | 持久化 | 是否結束 | 分布式事務成功 |
|---|
| 正向流程 | Prepare | DB prepare MQ prepare | Yes | No | No | No |
| 正向流程 | Commit | DB commit MQ commit | Yes | Yes | Yes | Yes |
| - | - | - | - | - | - | |
| 例外流程 | Prepare | DB 或 MQ 例外 | No | No | Yes | No |
| 例外流程 | Commit | DB提交例外 | No | No | Yes | No |
| 例外流程 | Commit | MQ提交超時 回呼本地事務狀態,本地事務成功則提交MQ事務 | Yes | Yes | Yes | Yes |
| 例外流程 | Commit | MQ提交超時 回呼本地事務狀態,本地事務失敗則取消MQ事務 | Yes | No | Yes | No |
- Q:會不會存在MQ事務成功,本地事務失敗?
A:不會,而且要避免這種情況發生,兩個2PC事務的prepare準備階段可以同時發起,但在commit提交階段要先保證本地資料庫事務成功之后再進行MQ事務訊息的commit,也就是在commit階段是存在依賴關系、串行化之行的
- Q:事務訊息如何確保最終一致?
A:prepare階段失敗、本地事務commit階段則均不會持久化;當prepare階段成功、本地事務commit成功,此時MQ的commit階段例外,則依賴MQ事務訊息的超時commit機制觸發回呼本地事務狀態介面方法來決定MQ的二階段是commit還是cancel
1.3 TCC(Try-Commit-Cancel)
互動流程

TCC事務其實主要包含兩個階段:Try階段、Confirm/Cancel階段,
- ① try-嘗試執行業務
完成所有業務檢查(一致性)
預留必須業務資源(準隔離性) - ② confirm-確認執行業務
真正執行業務
不作任何業務檢查
只使用Try階段預留的業務資源
Confirm操作必須保證冪等性 - ③ cancel-取消執行業務
釋放Try階段預留的業務資源
Cancel操作必須保證冪等性
從TCC的邏輯模型上我們可以看到,TCC的核心思想是,Try階段檢查并預留資源,確保在Confirm階段有資源可用,這樣可以最大程度的確保Confirm階段能夠執行成功,這里的資源可以是DB,或者MQ,RPC,只要是分布式環境中的事務載體就可以,而且需要這些分布式事務的參與者具備正逆向條件,比如DB、MQ的事務可以支持2PC,可以根據協調者對其進行事務提交或者取消操作,RPC比如賬戶服務可以支持正向增加也可以支持逆向減少,除此之外,DB、MQ要自身支持事務的ACID,這是參與分布式事務的原子性保證,RPC底層也是DB,這里可以等同理解,以上參與者具備參與分布式事務的基本條件后便可以進行整合和使用,業務流程的驅動和事務的完整性由中間協調者來操作和推進,
場景:積分商品兌換

專案背景
這是一個電商系統比較經典的下單、扣款、發貨流程,在下單之前會通過一系列商品在架狀態、庫存數量、購買限制等有效性過濾,然后在業務系統中進行一個商品兌換的場景,
分布式事務
由于是商品兌換,對于用戶和系統本身來說是這個程序一個原子性的、一鍵完成的操作,因此下單+動賬+發貨是一個現實存在的分布式事務,這里假設訂單資料和動賬資料在一個本地資料庫事務中,持久化開啟資料庫本地事務,該事務中記錄訂單生成資料和動賬資料資訊,以及發貨狀態的資訊記錄,這里需要解決的分布式事務是訂單資料、動賬資料、發貨狀態三種的最終一致,
設計方案
| 資料 | 系統 | 事務 | 正向流程 | 逆向流程 |
|---|
| 訂單資料 | 本地系統 | 本地事務 | 本地事務2PC保證 | 定時任務,例外關單 |
| 動賬資料 | 本地系統 | 本地事務 | 本地事務2PC保證 | 定時任務,逆向補賬 |
| 發貨狀態 | 外部系統 | 分布式事務 | 聯同本地事務提交 / 定時任務補償提交 | 定時任務,驅動發歡訓取消 |
場景Q&A
- Q:動賬扣款成功,但是發貨失敗怎么辦?
A: 定時任務根據發貨狀態進行發貨流程驅動
- 定時任務補償再次發貨
發貨成功則分布式事務最終一致,下游發貨系統進行發貨防重處理 - 發貨失敗進入逆向流程
定時任務驅動發貨最終一致理論上可以一直進行,但是發貨可能有一個流程時效和庫存售罄的問題,可以根據業務場景進行配置比如2天內重復呼叫失敗或呼叫回傳無庫存則進入逆向關單退款流程使得分布式事務恢復成最開始的一致性狀態
- Q:逆向流程回滾賬戶扣款,怎么防重?
A:賬戶流水表做唯一索引正逆向型別 + 業務ID,和賬戶額度表進行本地事務操作,確保只能成功一筆正向/逆向業務操作
場景:廣告任務結算

專案背景
當一個App有了足夠多的用戶體量,便開始有商家進行廣告或商品的投放,平臺通過包裝運營活動、廣告位等,將用戶曝光與商家付費結合達到流量變現的目的,
分布式事務
當用戶進行瀏覽、關注、商品購買、視頻觀看、App下載等多種任務,我們會根據商家配置等付費規則進行商家廣告費用的扣減,同時還要為用戶完成任務進行獎勵結算,此時的分布式事務便是商家賬戶扣減與用戶賬戶增加的資料一致性問題
設計方案
| 角色 | 資料 | 系統 | 操作 |
|---|
| 平臺 | 業務流水 | 結算系統 | 扣減商戶費用、增加用戶余額 |
| 商戶 | 商戶余額 | 商戶系統 | 扣減費用 |
| 用戶 | 用戶余額 | 用戶系統 | 增加余額 |
場景Q&A
- Q:商戶扣款失敗或者商戶扣款成功,用戶結算失敗怎么辦?
A: 定時任務根據結算狀態進行結算流程驅動,會一直輪詢補償嘗試結算用戶,直到成功,
- Q:商戶扣款成功,用戶結算失敗為何不進行做業務超時的逆向流程回滾商家扣款?
A: 用戶做任務時一定是做了前置校驗進行平臺任務發放的,也就是說用戶任務只要完成必須要進行結算,這是一個不能逆向的流程,即使極端情況下商戶余額空了暫時無法結算到用戶也要一直重試,一旦商戶余額充足則繼續整個結算流程,
場景:運營活動抽獎

專案背景
抽獎是運營活動中比較常見的方式,對于用戶來說需要做的是參加設定人物獲取積分,攢夠積分就可以開始抽獎,抽中后即等待獎品入賬,一般券會立刻入賬使用,而實物商品則需要耐心等待物流送到用戶手上,
分布式事務
關于抽獎,涉及賬戶動賬、庫存參與次數扣減、抽獎等多個環節,該場景要解決的分布式事務是賬戶動賬、活動庫存變更、抽獎下單資料一致性,
設計方案
| 資料 | 系統 | 操作 |
|---|
| 賬戶余額 | 交易系統 | 扣減賬戶余額 |
| 活動庫存 | 運營活動系統 | 扣減庫存、用戶日參&總參 |
| 業務流水 | 運營活動系統 | 業務ID[動賬憑證],抽獎ID[抽獎憑證],活動ID |
| 抽獎流水 | 抽獎系統 | 抽獎下單,獲取中獎結果 |
場景Q&A
- Q:動賬扣減失敗,補償流程為何不驅動扣減完成抽獎?
A: 抽獎業務對于用戶來說實時性要求很高,正向流程沒有完成的話,沒有通過補償流程驅動的必要性了,用戶體驗不好容易產生問題,補償流程只針對賬戶扣減成功扣沒有順利完成抽獎的情況進行賬戶補款即可,而且這部分補款是有延遲的,在C端展示可以優化或者忽略合并掉,保證的是賬戶額度無損,
2. 弱一致性
2.1 最大努力通知 + 訊息重試控制
場景:資料變更同步下游業務方

專案背景
系統資料發生變更時,會對外部系統產生一定影響,外部系統需要知道這種資料變化,這便是資料狀態同步的場景,一般來說資料互動可以有推(Push)、拉(Pull) 兩種形式,這里先說推模式,即資料變更方負責將變化通知到資料關注方,
分布式事務
這里要保證的是資料變更在多個應用中的狀態一致,
設計方案
| 角色 | 驅動方式 | 通信方式 |
|---|
| 資料變更方 | MQ + RPC 最終一致 | 呼叫介面通知其他系統 |
| 資料關注方 | RPC呼叫成功更新資料 | 提供介面接收資料變更 |
場景Q&A
這里是弱一致性的實作,沒有做本地事務表和定時任務輪詢對比各事務狀態進行補償操作,完全依賴于MQ的失敗重試驅動,若RPC呼叫失敗即資料同步業務方失敗,MQ會一直進行重試操作,隨著重試次數增加,重試間隔也會增加,這里也可以業務自行進行最大努力嘗試次數的控制,超過多少次嘗試仍失敗則放棄,因此不能保證最終一致,
場景:資料變更廣播下游業務方

專案背景
這里是資料同步的說拉模式,即資料關注方對資料變更方進行資料狀態變更的監聽,這種處理方式處理的主動權在于資料關注方,資料變更方只負責和保證一定通知到資料變更情況,是否能夠同步成功則由監聽方處理和兼容,
分布式事務
這里要保證的是資料變更在多個應用中的狀態一致,
設計方案
| 角色 | 驅動方式 | 通信方式 |
|---|
| 資料變更方 | 生產MQ | 資料變更存入MQ佇列 |
| 資料關注方 | 監聽MQ | 消費MQ處理資料變更情況 |
場景Q&A
這里也是弱一致性的實作,沒有做本地事務表和定時任務輪詢對比各事務狀態進行補償操作,完全依賴于MQ消費方的處理,若消費方處理失敗或在訊息佇列規定時間內沒有消費完畢,則資料無法保證最終一致,
2.2 戰略放棄 + 報警 + 人工修復
場景:秒殺庫存回滾

專案背景
在秒殺場景中,最復雜的除了解決高并發問題外,最核心的就是活動商品的庫存控制、變更問題,一般商品庫存會初始化到Redis快取中進行管理,秒殺方法會對Redis快取庫存數量進行校驗、扣減操作,通過MQ異步扣減DB庫存,既利用Reids原子操作進行庫存數量操作,又利用快取抗住高并發請求,起到異步削峰的作用,這是秒殺的正向流程,而逆向流程是用戶秒殺到商品預占了庫存,但是沒有及時進行訂單支付或者進行了訂單取消,此時要發起對庫存的恢復操作,
分布式事務
這里的分布式事務是Redis快取庫存與DB庫存數量一致性問題,
設計方案
| 存盤介質 | 庫存操作方式 |
|---|
| Redis | incr 、decr累計或扣減 |
| DB | MQ異步扣減 |
場景Q&A
-
Q:秒殺場景會出現哪些分布式問題?
A: 根據如上流程圖,扣減快取庫存、創建訂單、異步MQ發送是在一個同步的函式方法中的三個非原子的子步驟,而秒殺場景下流量洪峰會一瞬間打滿執行緒,以上三個子步驟任何異步都會出現問題,因為都是先扣快取庫存數量,根據實踐經驗看,極端情況下會出現扣減快取庫存成功了,后面創建訂單失敗了或者異步MQ沒有發出來無法削減DB庫存數量,因此資料結果是快取庫存扣減的多,DB扣減的少,實際搶購賣出的少,換句話說就是出現了少賣的現象,
-
Q:會不會出現超賣現象?
A: 不會,依賴于Redis單執行緒命令執行的保證,這里需要注意的是讀、寫命令不是一致,可以結合分布式鎖實作,也可以通過Lua腳本實作命令的原子性執行,
這里也是一個弱一致性的實作,業務場景我們保證不超賣即可,對于極端情況出現的庫存數量無效多扣減做戰略性放棄,一般情況下不會影響大多業務使用,如果非要吹毛求疵達到賬戶金額那種強一致性,思路也很簡單,可以借助定時任務輪詢對比快取與DB庫存數量進行校驗,這里還要考慮到其他在行流程如超市關單庫存恢復,仍然在行的秒殺活動等,保證資料處理不多加不多減,
3. 總結
3.1 分布式角色
- 參與者
可以通俗的認為是DB、RPC、MQ這些能夠提供事務能力的中間件或介面服務 - 協調者
維系分布式事務各個參與者分布式狀態的系統、中間件,如Zookeeper、業務系統
3.2 技術保證
- 資料庫事務 資料庫如MYSQL提供了2PC、XA協議,依賴于
WAL + Redo Log + 刷盤策略保證 - MQ事務 提供了2PC協議,依賴于
Ack機制+刷盤策略保證
3.2 強弱一致選擇
- 強一致性
強一致性確保的不是事務一定成功,而是事務參與者的子事務要么全成功,要么全失敗,保證子事務的最終一致,一般依賴于定時任務、補償機制、Double Check等方式進行事務狀態的校準和協調,一般設計和實作的復雜度大,參與者越多,流程越復雜,越難以維護,最終一致的延遲性和可靠性保證越難 - 弱一致性
弱一致性放棄了最終一致性的保證,通過最大努力實作而不保證最終的結果,這種場景減少和減低了開發和設計的復雜度
3.3 冪等&防重
- 業務冪等通常會定義
bizId代表全域唯一的業務標識,在MQ重發、重復消費、亂序,RPC重復呼叫等場景進行業務防重兼容處理, - 如賬戶余額的強一致防重處理,可以結合流水表唯一索引
正逆向型別 + 業務ID進行攔截 - 一般大多依賴于資料庫的唯一索引進行防重保證,如果擔心資料庫性能問題,可以前置快取攔截處理
3.4 盡早干預&補償一致
- 盡早干預
指的是代碼邏輯上盡早對串行處理的做個子事務進行回滾或逆向操作,這樣可以盡快結束分布式事務,而不需要等待相對更為延遲的定時任務或其他補償機制來驅動,這里可以使用旁路方法或不阻塞主方法放到MQ或異步執行緒中進行處理,比如秒殺下單發貨因為庫存不足或商品下架可以立刻進行發起關單退款的逆向流程 - 補償一致
補償機制一般可以通過定時任務、MQ重試來進行子事務驅動整個分布式事務的完結
5. 參考
《軟體架構設計:大型網站技術架構與業務架構融合之道》
用戶增長運營活動系統
電商大促秒殺活動系統
用戶中心認證系統