分布式事物一致性设计思路
?
本地事務ACID大家應該都知道了,統一提交,失敗回滾,嚴格保證了同一事務內數據的一致性!而分布式事務不能實現這種ACID,它只能實現CAP原則里的某兩個,CAP也是分布式事務的一個廣泛被應用的原型,CAP(Consistency, Availability, Partition Tolerance), 闡述了一個分布式系統的三個主要方面, 只能同時擇其二進行實現. 常見的有CP系統, AP系統。
關于分布最終一致性保證始終是分布式框架要考慮的問題。
分布式事物目前解決方案有三種,比較著名的有基于XA協議的方案、TCC方案、消息最終一致性方案。
2.1基于XA協議的方案
該方案最早由oracle提出用于解決跨數據訪問的事務問題,是一種強一致性的解決方案,由事務協調器和本地資源管理器共同完成。事務協調器和資源管理器間通過XA協議進行通信。XA協議實現的原理如下圖所示,共分為兩個階段,也就是我們常說的兩階段協議。
兩階段方案在解決數據庫分布式事務問題方面應用非常廣泛,oracle、Mysql等主流關系數據庫均支持XA協議,而且ocenbase、DCDB等著名的分布式數據庫也都基于兩階段協議。在解決服務事務問題上,其實 XA協議不是只能作用于單個服務內部的多資源場景,跨服務的多資源場景也是可以的,只不過需要額外的事務傳遞機制。但其都有致命的缺點,性能不理想。由于需要等到各分支事務都就緒后全局事務才開始提交,所以每個事務鎖定數據的時間較長,XA方案因此很難滿足高并發場景。而且在解決微服務問題時XA方案的性能問題將會被放大。因為應用在訪問服務的調用方式、網絡環境等要比訪問數據庫復雜的多。例如,應用和其訪問的數據庫通常在一個局域網中,而其通過rpc調用的服務則可能屬于另一個網絡或者在公網上,其時延更長、出故障的概率更高。這將導致數據鎖定時間和系統并發度進一步降低。所以XA方案基本不適合解決微服務的事務問題。
2.2TCC方案
TCC方案應用是目前呼聲最高,也是落地最多的一個方案。當前也有一些開源的TCC框架實現,如TCC-Transaction、ByteTCC。TCC方案其實是兩階段方案的一種改進,其將本地資源管理器的功能融入到了業務實現中。其將整個業務邏輯顯示的分成了Try、Confirm、Cancel三部分。try部分完成業務的準備工作,confirm部分完成業務的提交,cancel部分完成事務的回滾。基本原理如下圖所示。
事務開始時,業務應用會向事務協調器注冊啟動事務。之后業務應用會調用所有服務的try接口,相當于XA的第一階段。如果有任何一個服務的try接口調用失敗會向事務協調器發送事務回滾請求,否則發送事務提交請求。事務協調器收到事務回滾請求后會依次調用事務的confirm接口,否則調用cancel接口回滾,這相當于XA的第二階段。如果第二階段接口調用失敗,會進行重試。
TCC方案通過通過三個接口很好的規避了長時間數據加鎖的問題,業務表在每個接口調用完畢即可釋放,這很大程度上提高了業務的并發度,這也是TCC方案最大的優勢。所以在SOA時期,TCC方案被很多金融、電商的業務系統大量使用。
當然TCC方案也有不足之處,集中表現在以下兩個方面:
- 開發工作量大。它將部分資源管理器的功能融入到每個服務的開發中,導致服務的每個接口都需要實現try、confirm、cancle,還需要實現事務協調器,開發量不只翻了一倍。
- 實現難度大。系統需要記錄每個應用的服務調用鏈路。我前面講過rpc調用情況比較復雜,由于網絡狀況、系統故障等調用失敗被視為常態,必須按照不同的失敗原因實現不同策略的回滾。為了滿足一致性的要求,二階段不管調用confirm還是cancle都必須調用成功,如果一次調用不成功,事務協調器必須嘗試重試。這就要求confirm和cancle接口必須實現冪等。
上述原因導致TCC方案大多是被研發實力較強、有迫切需求的大公司所采用。其將分布式事務變成一種所謂的“貴族技術”,中小型企業由于人員有限、技術實力薄弱,很難落地。而且筆者認為微服務倡導的是服務的輕量化、易部署,而TCC方案將很多事務的處理功能融入到業務中,對業務侵入性太高,導致服務邏輯復雜,比較適合比較重的服務。
2.3 消息事務一致性方案
消息一致性方案是通過消息中間件保證上、下游應用數據操作的一致性。基本思路是將本地操作和發送消息放在一個事務中,保證本地操作和消息發送要么兩者都成功或者都失敗。下游應用向消息系統訂閱該消息,收到消息后執行相應操作。
以下單業務為例進行說明,下單基本流程是先存儲訂單信息,然后扣相應商品的庫存,兩個操作必須在一個事務中。如下圖,業務應用首先調用訂單服務,訂單存儲成功后,訂單服務會通過消息處理服務投遞訂單消息到MQ。庫存服務從MQ收到消息后進行扣庫存操作,如果執行成功會向消息處理服務發送通知。消息處理服務會實時監測訂單消息是否超時,如果超時會重新投遞到MQ中,以驅動庫存服務進行扣庫存操作。如果扣庫存操作執行失敗后,庫存服務后續還會從MQ接收到相同的訂單消息,需要多次重復執行,直到成功或者進行人工干預。庫存服務需要實現冪等。?
消息方案從本質上講是將分布式事務轉換為兩個本地事務,然后依靠下游業務的重試機制達到最終一致性。相對TCC方案來講,消息方案技術難度相對低,落地較容易,如果對一致性不敏感的應用也是一個不錯的選擇。美國著名電商e-bay以及國內的蘑菇街都做過嘗試。消息一致性方案的不足之處是其對應用侵入性較高,應用需要基于消息接口進行改造,而且需要建設專門的消息系統,成本較高。
?
目前已有基于TCC設計方案可參考:
https://github.com/changmingxie/tcc-transaction
?
下面是轉自大鵬設計師基于TCC實現的設計思路,考慮的更加全面:詳見:https://github.com/dapeng-soa/dapeng-soa/wiki/TCC-support
1、基本概念
TI:Transaction Interceptor,事務攔截器,位于dapeng容器的filterChain鏈中。
由于TI的邏輯會比較復雜, 不太適合在IO線程中操作
TM:Transaction Manager, 事務管理器,作為一個獨立的服務存在。
事務發起方: 服務調用鏈或者說請求會話中第一個加入全局事務的接口方法,稱為事務發起方。
事務參與方: 服務調用鏈或者說請求會話中除事務發起方的其它加入了全局事務的接口方法,稱為事務參與方。
例如,對于服務a,b,c, d: client調用a.m1, a.m1調用b.m2以及c.m3, b.m2調用d.m4. 其中,a.m1以及b.m2,d.m4都聲明為TCC事務, 那么在這次服務調用中, a.m1為事務發起方,b.m2,d.m4為事務參與方。
由事務參與方發起confirm或者cancel操作。
事務管理器負責confirm或者cancel失敗后的重試。
在定義接口的時候, 需要加上以下注解,以表明該接口需要加入全局事務。@TCC(confirm="",cancel="")?該注解有2個可選參數, 其中, confirm代表該接口的confirm方法名字,cancel代表該接口的cancel方法名字。
默認情況下,methodA的confirm方法名為methodA_confirm, cancel方法名為methodA_cancel
2、數據表結構
t_gtx
CREATE TABLE IF NOT EXISTS `mydb`.`t_gtx` (`id` INT(11) NOT NULL,`gtx_id` INT(11) NOT NULL COMMENT '全局事務id,一般使用服務的會話id(sesstionTid)',`status` SMALLINT(2) NOT NULL DEFAULT 1 COMMENT '全局事務狀態, 1:新建(CREATED);2:成功(SUCCEED);3:失敗(FAILED);4:完成(DONE)',`expired_time` DATETIME(0) NOT NULL COMMENT '超時時間。事務管理器的定時任務會根據全局事務表的狀態以及超時時間去過濾未完成且超時的事務。默認為事務創建時間后1分鐘。',`created_time` DATETIME(0) NOT NULL COMMENT '創建時間',`updated_time` TIMESTAMP(0) NOT NULL DEFAULT DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新時間',`remark` VARCHAR(255) NULL COMMENT '備注, 每次狀態變更都需要追加到remark字段。',PRIMARY KEY (`id`),INDEX `index_gtx_id` (`gtx_id` ASC)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '全局事務表't_gtx_step
CREATE TABLE IF NOT EXISTS `gtx_db`.`t_gtx_step` (`id` INT NOT NULL,`gtx_id` INT(11) NOT NULL COMMENT '全局事務id,一般使用服務的會話id(sesstionTid)',`step_seq` SMALLINT(2) NOT NULL COMMENT '子事務序號',`status` SMALLINT(2) NOT NULL DEFAULT 1 COMMENT '子事務狀態, 1:新建(CREATED);2:成功(SUCCEED);3:失敗(FAILED);4:完成(DONE)',`service_name` VARCHAR(128) NOT NULL COMMENT '服務名',`version` VARCHAR(32) NOT NULL DEFAULT '1.0.0' COMMENT '服務版本號',`method_name` VARCHAR(32) NOT NULL,`request` BLOB NULL,`confirm_method_name` VARCHAR(32) NULL,`cancel_method_name` VARCHAR(32) NULL,`redo_times` INT(11) NOT NULL DEFAULT 0,`created_time` DATETIME(0) NOT NULL COMMENT '創建時間',`updated_time` TIMESTAMP(0) NOT NULL DEFAULT DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新時間',`remark` VARCHAR(45) NOT NULL DEFAULT '' COMMENT '備注, 每次狀態變更都需要追加到remark字段。',PRIMARY KEY (`id`)),INDEX `index_gtx_id` (`gtx_id` ASC)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '全局事務流程表't_gtx_journal?對于參與分布式事務的服務接口,需要在本地有個事務流水表(例如orderDb):
CREATE TABLE IF NOT EXISTS `mydb`.`t_gtx_journal` (`id` INT(11) NOT NULL,`gtx_id` INT(11) NOT NULL COMMENT '全局事務id',`step_id` INT(11) NOT NULL COMMENT '子事務id',`biz_tag` VARCHAR(45) NOT NULL COMMENT '本次全局事務操作的本地業務表名字',`biz_id` INT(11) NOT NULL COMMENT '本次全局事務操作的本地業務記錄id',`status` SMALLINT(2) NOT NULL DEFAULT 1 COMMENT '本地子事務狀態, 可在confirm/cancel階段用于判斷try階段是否成功 1:新建(CREATED);4:完成(DONE)',`old_values` VARCHAR(255) NULL COMMENT '修改前的值。可選,用于在cancel階段恢復原始值。例如修改字符串的操作。格式為:fieldName:fieldValue fieldName:fieldValue',`created_time` DATETIME(0) NOT NULL COMMENT '創建時間',`updated_time` TIMESTAMP(0) NOT NULL DEFAULT DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新時間',`remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '備注, 每次狀態變更都需要追加到remark字段。',PRIMARY KEY (`id`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '子事務的本地流' /* comment truncated */ /*水表。 當本地事務成功時, 由本地業務*/本流水表可用于冪等(例如confirm或者cancel的重試,如果狀態是完成,那么就不需要執行confirm/cancel邏輯, 或者可用于判斷try階段是否成功。
本地事務流水是否需要創建,需要創建多少,是否記錄oldValues,根據業務性質去定。 例如, 創建訂單的時候,會創建一個主單若干個子單。 這時候, 只需要插入一條本地事務流水(跟主單掛鉤)即可。 因為在confirm或者cancel中, 根據主單id可以招到所有的子單id。
3、案例描述
這里以訂單創建為例。
用戶創建訂單,同時扣除庫存。
其中訂單、庫存分別為兩個不同的服務。同時, TM也是一個單獨的服務。
本流程有2個業務服務參與,分別是訂單服務的創建訂單接口以及庫存服務的庫存扣減接口。
業務主流程如下:
1、客戶端調用orderService.createOrder, 發起訂單創建流程 2、orderService調用stockService.decreaseStock, 扣減庫存 3、orderService創建訂單,并返回客戶端。對應的訂單創建序列圖如下:?
3.1. 客戶端發起訂單創建的操作
對應時序圖的No.1調用
參數
3.2、全局事務的Try階段
訂單服務的全局事務攔截器(TI)收到請求后, 識別到目標方法帶有TCC標識,即進入Trying階段。
3.2.1、訂單服務開啟全局事務
TI向事務管理服務請求開啟全局事務,對應時序圖的No.2。?tm.beginGTX(gtxId, params)
txId可用sessionTid(long的形式),params可直接用bytes
3.2.2、事務管理器處理訂單服務請求
對應時序圖的No.3/4/5
事務管理器根據txId去決定調用方是事務發起者還是事務參與者。 這里,orderService是事務發起方, 那么: 1、TM首先通過createTGX(txId)方法創建一個全局事務(插入一條全局事務記錄到t_gtx表中,狀態為新建) 2、通過createStep(txId, params)方法創建一個子事務日志(插入一條子事務記錄到t_gtx_step表中, 狀態為新建)
全局事務開啟, 操作成功后返回stepId繼續下一步,否則失敗后直接返回調用方,由調用方決定是繼續還是回滾(在這個案例中, 這里的調用方是client)。
3.2.3、訂單服務的TI轉發請求到具體的業務服務方法
對應時序圖中的No.6/7 全局事務開啟成功后, TI轉發請求到業務服務。這里為orderService.createOrder。
在這個方法中, 首先調用庫存服務的扣減庫存接口:stockService.decreaseStock
如果全局事務開啟失敗,那么TI會直接報錯返回給調用方(Err-Gtx-001: begin gtx error)
3.2.4、庫存服務開啟全局事務
對應時序圖的No.8
同3.2.1,庫存服務的TI收到扣減庫存請求后,開啟全局事務: `tm.beginGTX'
3.2.5、事務管理器處理庫存服務請求
對應時序圖的No.9/10
事務管理器通過gtxId發現全局事務已經開啟,那么該請求來自事務參與方而不是發起方。 這時候,直接通過createStep插入一條子事務日志到t_gtx_step表中即可,并返回stepId。
3.2.6、庫存服務本地邏輯處理
對應時序圖的No.11/12/13
TI開始全局事務成功后, 轉發扣減庫存請求給具體的業務方法。 庫存服務執行本地事務(庫存余額扣減,凍結庫存增加)后返回到TI
同時,需要插入一條本地事務流水表到t_gtx_journal中,
INSERT INTO `t_gtx_journal` (`id`, `gtx_id`, `step_id`, `biz_tag`, `biz_id`, `status`, `old_values`) VALUES (id, gtxId, stepId, 't_stock', stockId, 1, NULL);本案例不需要記錄oldValues, 因為根據接口的入參可以推算出oldValues
3.2.7、庫存服務的TI更新全局事務
對應時序圖的No.14/15/16
TI根據3.2.6的結果,調用tm.updateGTX更新全局事務。
TM根據gtxId以及stepId判斷該請求來自事務參與方,那么僅更新子事務日志表updateStep, 狀態為成功/失敗。
這一步有可能失敗,導致本地子事務提交后,結果沒反映到TM的子事務表的狀態中。
還有一個可能就是本地子事務成功,TI更新全局事務也成功了, 但是由于網絡中斷或者其他原因,導致服務調用方(這里是orderService)的對扣減庫存調用失敗。
不管如何,服務調用方調用失敗后,由服務調用方自行決定是繼續前行還是回滾全局事務。
3.2.8、訂單服務本地業務邏輯處理
對應時序圖的No.18/19
訂單服務根據庫存扣減的結果,決定是繼續往前走還是失敗回退。
如果繼續往前走的話,就完成本地事務后返回結果給訂單服務的TI; 如果失敗回退的話,就把失敗信息返回給訂單服務的TI。
3.2.9、訂單服務的TI更新全局事務
對應序列圖的No.20/21/22/23
如果訂單服務本地事務成功,那么TI通過tm.updateGTX把結果反饋給TM。
TM根據gtxId判斷該請求來自事務發起方,那么根據status把全局事務狀態更新為成功/失敗; 同時, 更新子事務狀態為成功/失敗
全局事務的最終狀態跟事務發起方對應的子事務的最終狀態一致。
No.20中如果事務發起方更新全局事務狀態失敗, 那么應通過實時告警的方式提醒人工介入,同時放棄confirm或者cancel操作, 直接返回前端(根據 根據事務發起方的本地事務流水狀態,更新全局事務狀態為成功/失敗(也需要更新事務發起方的子事務狀態)。 后續,TM定時器會處理后續的confirm或者cancel操作。
至此,Trying階段完成。
根據本階段的結果, TI將會進入TCC的confirm(成功)或者cancel階段(失敗)
3.3、confirm階段
對應序列圖的No.24~33 理論上, Trying階段成功的話,confirm階段一定能成功(最終一致).
Confirm操作由TI發起,而具體的邏輯由TM控制。
3.3.1 事務管理器的confirm操作
首先事務管理器根據gtxId得到全局事務記錄以及子事務記錄集合(gtx_steps)。
按照子事務的seq從小到大的順序,依次調用子事務的confirm方法。(這個過程可以使用異步的方式并發去confirm?)
最后根據結果更新全局事務以及子事務的狀態。
只有全部子事務的狀態為完成,全局事務狀態才能更新為完成。
TI發起confirm操作后,不管本次confirm操作是否成功, 都返回成功給client。
3.4、cancel階段
對應序列圖的No.24~43 本階段跟confirm階段邏輯類似,但是子事務的執行順序相反。
TI發起cancel操作后,不管本次cancel操作是否成功, 都返回失敗給client。
3.5、confirm/cancel階段的異常處理
TM通過定時器,定時掃描全局事務日志表中狀態為非完成的記錄(1分鐘前),再次執行confirm/cancel操作。
4. 業務場景
TCC場景:
4.1. 客戶端調用單獨的TCC服務
4.1.1 正常流程
try成功,confirm成功
try失敗,cancel成功
4.1.2 異常流程
try成功,confirm階段或者cancel階段失敗?那么后續由TM定時任務繼續重試。
4.1.3 異常流程
try階段TI插入事務日志失敗(Err-Gtx-001: begin gtx error)?如果是事務發起方(本案例), 那么TI直接返回Err-Gtx-001,本次服務調用失敗。 如果是事務參與方, 那么TI直接返回Err-Gtx-001,并最終回到事務發起方,本次全局事務失敗,并對已經有記錄的子事務做cancel操作。
因為這里缺失了分布式事務的某個子事務日志記錄,TM無法進行confirm或者cancel操作。
try階段本地事務成功,但是TI更新事務日志失敗(Err-Gtx-002: update gtx error),子事務的狀態停留在新建的狀態?這時候如果是事務發起方(本案例),那么TI會繼續走confirm或者cancel的流程。 如果是事務參與方,把Err-Gtx-002返回, 事務發起方會忽略該錯誤,其對應的TI會繼續走confirm或者cancel的流程。
在confirm或者cancel的邏輯里,TM會把gtxId以及該子事務id、狀態通過cookie傳過來。 如果子事務狀態為成功或者失敗,那么直接執行confirm或者cancel邏輯;
如果子事務狀態為新建,那么目前尚不清楚到底try階段的本地事務執行了沒。
如果執行了, 那么必然可以通過gtxId,stepId找到在try階段的本地事務操作過的本地事務流水記錄,從而確認try階段的本地事務提交情況,再進而決定本次confirm或者cancel該做的操作。
舉個例子, 庫存服務的扣減庫存接口。 在try階段,本地事務成功,然后TI在更新子事務狀態的時候失敗了,那么該子事務狀態為新建。 然后事務發起方依然決定做confirm操作,同時庫存服務扣減庫存接口的confirm方法,通過gtxId以及stepId,找到了本地事務流水記錄,從而可以執行confirm操作。
如果在try階段,本地事務失敗,然后TI在更新子事務狀態的時候也失敗了,那么該子事務狀態為新建。 然后事務發起方依然決定做confirm操作,同時庫存服務扣減庫存接口的confirm方法,通過gtxId以及stepId,這時候是找不到本地事務流水記錄的,說明try階段本地事務失敗。 那么業務可以調用一下把try以及confirm的邏輯合并起來,完成本次confirm操作。
4.2. 客戶端先后調用2個TCC服務
這時候, 這兩次服務調用分別構成一個全局事務, 是兩個互不相關的全局事務
4.3. 客戶端調用TCC服務a,服務a再調用TCC服務b
4.4. 客戶端調用TCC服務a,服務a再分別調用TCC服務b以及TCC服務c
4.5. 客戶端調用TCC服務a,服務a調用TCC服務b,服務b再調用TCC服務c
?
轉載于:https://www.cnblogs.com/barrywxx/p/10257286.html
總結
以上是生活随笔為你收集整理的分布式事物一致性设计思路的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SQL Server含逗号分隔的数据匹配
- 下一篇: 2018-2019-2 网络对抗技术 2