分布式框架下,如何保證事物一致性一直是一個熱門話題。當然事物一致性解決方案有很多種(請參考:分布式事物一致性設計思路),我們今天主要介紹TCC方案解決的思路。以下是參與設計討論的一種解決思路,大家有問題請留言。
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="", asyncCC="true")
該注解有3個可選參數, 其中, confirm代表該接口的confirm方法名字,cancel代表該接口的cancel方法名字,asyncCC代表CC階段是否采用異步方式。
默認情況下,methodA的confirm方法名為methodA_confirm, cancel方法名為methodA_cancel, asyncCC默認為true
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分鍾。',
`async` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否異步confirm/cancel,默認是',
`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 對於參與分布式事務的服務接口,需要在本地有個事務流水表 本流水表可用於冪等(例如confirm或者cancel的重試,如果狀態是完成,那么就不需要執行confirm/cancel邏輯),或者在confirm/cancel邏輯中找到之前try階段修改過的記錄。
該流水表跟業務密切相關且應用在業務邏輯上(框架本身不操作該表),可由業務團隊自行設計(甚至表名也可以自定義)。
下面給出一個參考實現 (例如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 */ /*水表。 當本地事務成功時, 由本地業務*/
本地事務流水是否需要創建,需要創建多少,是否記錄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(params)
全局事務開啟失敗的話, 返回Err-Gtx-001: Begin gtx err。
gtxId通過TransactionContext傳過去(如果存在的話), params可直接用bytes
3.2.2、事務管理器處理訂單服務請求
對應時序圖的No.3/4/5
事務管理器根據TransactionContext是否含有gtxId去決定調用方是事務發起者還是事務參與者。 這里,orderService是事務發起方, 那么: 1、TM首先生成全局唯一的gtxId,通過createGTX(gtxId)方法創建一個全局事務(插入一條全局事務記錄到t_gtx表中,狀態為新建) 2、通過createStep(txId, params)方法創建一個子事務日志(插入一條子事務記錄到t_gtx_step表中, 狀態為新建)
全局事務開啟, 操作成功后返回(gtxId, 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'
如果本子事務在加入全局事務時失敗, 那么由調用端決定是否繼續執行全局事務。 如果繼續執行全局事務的其它子事務, 那么后續在CC階段,本子事務將不會confirm或者cancel
TimeOut怎么辦 建議事務發起者做cancel處理。
3.2.5、事務管理器處理庫存服務請求
對應時序圖的No.9/10
事務管理器通過gtxId發現全局事務已經開啟,那么該請求來自事務參與方而不是發起方。 這時候,直接通過createStep
插入一條子事務日志到t_gtx_step表中即可,並返回(gtxId,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、訂單服務本地業務邏輯處理
對應時序圖的No.14/15/16
訂單服務根據庫存扣減的結果,決定是繼續往前走還是失敗回退。
如果繼續往前走的話,就完成本地事務后返回結果給訂單服務的TI; 如果失敗回退的話,就把失敗信息返回給訂單服務的TI。
至此,Trying階段完成。
根據本階段的結果, TI將會進入TCC的confirm
(成功)或者cancel
階段(失敗)
3.3、confirm階段
對應序列圖的No.17~30 理論上, Trying階段成功的話,confirm階段一定能成功(最終一致).
Confirm操作由TI發起,而具體的邏輯由TM控制。
3.3.1 事務管理器的confirm操作
首先事務管理器根據gtxId
得到全局事務記錄以及子事務記錄集合(gtx_steps
)。
然后通過獨立的事務,把全局事務狀態更新為"成功"
然后按照子事務的seq從小到大的順序,依次異步調用子事務的confirm方法。 在異步回調中根據調用結果,如果confirm成功,那么更新子事務的狀態為"完成"
只有全部子事務的狀態為完成,全局事務狀態才能更新為完成。
TI發起confirm操作后,不管本次confirm操作是否成功, 都返回成功給client。
3.4、cancel階段
對應序列圖的No.31~44 本階段跟confirm階段邏輯類似,但是子事務的執行順序相反。
TI發起cancel操作后,不管本次cancel操作是否成功, 都返回失敗給client。
3.5、confirm/cancel階段的異常處理
TM通過定時器,定時掃描全局事務日志表中狀態為非完成的記錄(5分鍾前),再次執行confirm/cancel操作。
4. 業務場景
TCC場景:
4.1. 客戶端調用單獨的TCC服務
4.1.1 正常流程
try成功,confirm成功
- try階段:
1.1 t_gtx, t_gtx_step插入事務日志成功, 狀態皆為新建
1.2 tccServiceA本地事務成功
- confirm階段 2.1 TM調用tccServiceA成功,更新t_gtx, t_gtx_step成功,狀態為完成。
try失敗,cancel成功
- try階段:
1.1 t_gtx, t_gtx_step插入事務日志成功, 狀態皆為新建
1.2 tccServiceA本地事務失敗
- cancel階段 2.1 TM調用tccServiceA成功,更新t_gtx, t_gtx_step成功,狀態為完成。
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/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
問題
定時器發起的全局事務, 不經過TI。。。
定時器可通過客戶端的方式調用服務,而不是直接調用action。