1、基本概念
TI:Transaction Interceptor,事務(wù)攔截器,位于dapeng容器的filterChain鏈中。
由于TI的邏輯會比較復(fù)雜, 不太適合在IO線程中操作
TM:Transaction Manager, 事務(wù)管理器,作為一個獨立的服務(wù)存在。
事務(wù)發(fā)起方: 服務(wù)調(diào)用鏈或者說請求會話中第一個加入全局事務(wù)的接口方法,稱為事務(wù)發(fā)起方。
事務(wù)參與方: 服務(wù)調(diào)用鏈或者說請求會話中除事務(wù)發(fā)起方的其它加入了全局事務(wù)的接口方法,稱為事務(wù)參與方。
例如,對于服務(wù)a,b,c, d:
client調(diào)用a.m1, a.m1調(diào)用b.m2以及c.m3, b.m2調(diào)用d.m4.
其中,a.m1以及b.m2,d.m4都聲明為TCC事務(wù), 那么在這次服務(wù)調(diào)用中, a.m1為事務(wù)發(fā)起方,b.m2,d.m4為事務(wù)參與方。
由事務(wù)參與方發(fā)起confirm或者cancel操作。
事務(wù)管理器負責(zé)confirm或者cancel失敗后的重試。
在定義接口的時候, 需要加上以下注解,以表明該接口需要加入全局事務(wù)。
@TCC(confirm="",cancel="")
該注解有2個可選參數(shù), 其中, confirm代表該接口的confirm方法名字,cancel代表該接口的cancel方法名字。
默認情況下,methodA的confirm方法名為methodA_confirm, cancel方法名為methodA_cancel
2、數(shù)據(jù)表結(jié)構(gòu)
t_gtx
CREATE TABLE IF NOT EXISTS `gtx_db`.`t_gtx` (
`id` INT(11) NOT NULL,
`gtx_id` INT(11) NOT NULL COMMENT '全局事務(wù)id,一般使用服務(wù)的會話id(sesstionTid)',
`status` SMALLINT(2) NOT NULL DEFAULT 1 COMMENT '全局事務(wù)狀態(tài), 1:新建(CREATED);2:成功(SUCCEED);3:失敗(FAILED);4:完成(DONE)',
`created_time` DATETIME(0) NOT NULL COMMENT '創(chuàng)建時間',
`updated_time` TIMESTAMP(0) NOT NULL DEFAULT DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新時間',
`remark` VARCHAR(45) NULL COMMENT '備注, 每次狀態(tài)變更都需要追加到remark字段。',
PRIMARY KEY (`id`),
INDEX `index_gtx_id` (`gtx_id` ASC))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COMMENT = '全局事務(wù)表'
t_gtx_step
CREATE TABLE IF NOT EXISTS `gtx_db`.`t_gtx_step` (
`id` INT NOT NULL,
`gtx_id` INT(11) NOT NULL COMMENT '全局事務(wù)id,一般使用服務(wù)的會話id(sesstionTid)',
`step_seq` SMALLINT(2) NOT NULL COMMENT '子事務(wù)序號',
`status` SMALLINT(2) NOT NULL DEFAULT 1 COMMENT '子事務(wù)狀態(tài), 1:新建(CREATED);2:成功(SUCCEED);3:失敗(FAILED);4:完成(DONE)',
`service_name` VARCHAR(128) NOT NULL COMMENT '服務(wù)名',
`version` VARCHAR(32) NOT NULL DEFAULT '1.0.0' COMMENT '服務(wù)版本號',
`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 '創(chuàng)建時間',
`updated_time` TIMESTAMP(0) NOT NULL DEFAULT DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新時間',
`remark` VARCHAR(45) NOT NULL DEFAULT '' COMMENT '備注, 每次狀態(tài)變更都需要追加到remark字段。',
PRIMARY KEY (`id`)),
INDEX `index_gtx_id` (`gtx_id` ASC))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COMMENT = '全局事務(wù)流程表'
t_gtx_journal
對于參與分布式事務(wù)的服務(wù)接口,需要在本地有個事務(wù)流水表(例如orderDb):
CREATE TABLE IF NOT EXISTS `order_db`.`t_gtx_journal` (
`id` INT(11) NOT NULL,
`gtx_id` INT(11) NOT NULL COMMENT '全局事務(wù)id',
`step_id` INT(11) NOT NULL COMMENT '子事務(wù)id',
`biz_tag` VARCHAR(45) NOT NULL COMMENT '本次全局事務(wù)操作的本地業(yè)務(wù)表名字',
`biz_id` INT(11) NOT NULL COMMENT '本次全局事務(wù)操作的本地業(yè)務(wù)記錄id',
`created_time` DATETIME(0) NOT NULL COMMENT '創(chuàng)建時間',
`updated_time` TIMESTAMP(0) NOT NULL DEFAULT DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新時間',
`remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '備注, 每次狀態(tài)變更都需要追加到remark字段。',
PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COMMENT = '子事務(wù)的本地流' /* comment truncated */ /*水表。 當(dāng)本地事務(wù)成功時, 由本地業(yè)務(wù)*/
3、案例描述
這里以訂單創(chuàng)建為例。
用戶創(chuàng)建訂單,同時扣除庫存。
其中訂單、庫存分別為兩個不同的服務(wù)。同時, TM也是一個單獨的服務(wù)。
本流程有2個業(yè)務(wù)服務(wù)參與,分別是訂單服務(wù)的創(chuàng)建訂單接口以及庫存服務(wù)的庫存扣減接口。
業(yè)務(wù)主流程如下:
1、客戶端調(diào)用orderService.createOrder, 發(fā)起訂單創(chuàng)建流程
2、orderService調(diào)用stockService.decreaseStock, 扣減庫存
3、orderService創(chuàng)建訂單,并返回客戶端。
對應(yīng)的訂單創(chuàng)建序列圖如下:

3.1. 客戶端發(fā)起訂單創(chuàng)建的操作
對應(yīng)時序圖的No.1調(diào)用
參數(shù)
3.2、全局事務(wù)的Try階段
訂單服務(wù)的全局事務(wù)攔截器(TI)收到請求后, 識別到目標方法帶有TCC標識,即進入Trying階段。
3.2.1、訂單服務(wù)開啟全局事務(wù)
TI向事務(wù)管理服務(wù)請求開啟全局事務(wù),對應(yīng)時序圖的No.2。
tm.beginGTX(gtxId, params)
txId可用sessionTid(long的形式),params可直接用bytes
3.2.2、事務(wù)管理器處理訂單服務(wù)請求
對應(yīng)時序圖的No.3/4/5
事務(wù)管理器根據(jù)txId去決定調(diào)用方是事務(wù)發(fā)起者還是事務(wù)參與者。
這里,orderService是事務(wù)發(fā)起方, 那么:
1、TM首先通過createTGX(txId)方法創(chuàng)建一個全局事務(wù)(插入一條全局事務(wù)記錄到t_gtx表中,狀態(tài)為新建)
2、通過createStep(txId, params)方法創(chuàng)建一個子事務(wù)日志(插入一條子事務(wù)記錄到t_gtx_step表中, 狀態(tài)為新建)
全局事務(wù)開啟, 操作成功后返回stepId繼續(xù)下一步,否則失敗后直接返回調(diào)用方,由調(diào)用方?jīng)Q定是繼續(xù)還是回滾(在這個案例中, 這里的調(diào)用方是client)。
3.2.3、訂單服務(wù)的TI轉(zhuǎn)發(fā)請求到具體的業(yè)務(wù)服務(wù)方法
對應(yīng)時序圖中的No.6/7
全局事務(wù)開啟成功后, TI轉(zhuǎn)發(fā)請求到業(yè)務(wù)服務(wù)。這里為orderService.createOrder。
在這個方法中, 首先調(diào)用庫存服務(wù)的扣減庫存接口:stockService.decreaseStock
如果全局事務(wù)開啟失敗,那么TI會直接報錯返回給調(diào)用方(Err-Gtx-001: begin gtx error)
3.2.4、庫存服務(wù)開啟全局事務(wù)
對應(yīng)時序圖的No.8
同3.2.1,庫存服務(wù)的TI收到扣減庫存請求后,開啟全局事務(wù): `tm.beginGTX'
3.2.5、事務(wù)管理器處理庫存服務(wù)請求
對應(yīng)時序圖的No.9/10
事務(wù)管理器通過gtxId發(fā)現(xiàn)全局事務(wù)已經(jīng)開啟,那么該請求來自事務(wù)參與方而不是發(fā)起方。
這時候,直接通過createStep插入一條子事務(wù)日志到t_gtx_step表中即可,并返回stepId。
3.2.6、庫存服務(wù)本地邏輯處理
對應(yīng)時序圖的No.11/12/13
TI開始全局事務(wù)成功后, 轉(zhuǎn)發(fā)扣減庫存請求給具體的業(yè)務(wù)方法。
庫存服務(wù)執(zhí)行本地事務(wù)(庫存余額扣減,凍結(jié)庫存增加)后返回到TI
3.2.7、庫存服務(wù)的TI更新全局事務(wù)
對應(yīng)時序圖的No.14/15/16
TI根據(jù)3.2.6的結(jié)果,調(diào)用tm.updateGTX更新全局事務(wù)。
TM根據(jù)gtxId以及stepId判斷該請求來自事務(wù)參與方,那么僅更新子事務(wù)日志表updateStep, 狀態(tài)為成功/失敗。
這一步有可能失敗,導(dǎo)致本地子事務(wù)提交后,結(jié)果沒反映到TM的子事務(wù)表的狀態(tài)中。
還有一個可能就是本地子事務(wù)成功,TI更新全局事務(wù)也成功了, 但是由于網(wǎng)絡(luò)中斷或者其他原因,導(dǎo)致服務(wù)調(diào)用方(這里是orderService)的對扣減庫存調(diào)用失敗。
不管如何,服務(wù)調(diào)用方調(diào)用失敗后,由服務(wù)調(diào)用方自行決定是繼續(xù)前行還是回滾全局事務(wù)。
3.2.8、訂單服務(wù)本地業(yè)務(wù)邏輯處理
對應(yīng)時序圖的No.18/19
訂單服務(wù)根據(jù)庫存扣減的結(jié)果,決定是繼續(xù)往前走還是失敗回退。
如果繼續(xù)往前走的話,就完成本地事務(wù)后返回結(jié)果給訂單服務(wù)的TI;
如果失敗回退的話,就把失敗信息返回給訂單服務(wù)的TI。
3.2.9、訂單服務(wù)的TI更新全局事務(wù)
對應(yīng)序列圖的No.20/21/22/23
如果訂單服務(wù)本地事務(wù)成功,那么TI通過tm.updateGTX把結(jié)果反饋給TM。
TM根據(jù)gtxId判斷該請求來自事務(wù)發(fā)起方,那么根據(jù)status把全局事務(wù)狀態(tài)更新為成功/失??;
同時, 更新子事務(wù)狀態(tài)為成功/失敗
全局事務(wù)的最終狀態(tài)跟事務(wù)發(fā)起方對應(yīng)的子事務(wù)的最終狀態(tài)一致。
至此,Trying階段完成。
根據(jù)本階段的結(jié)果, TI將會進入TCC的confirm(成功)或者cancel階段(失敗)
3.3、confirm階段
對應(yīng)序列圖的No.24~33
理論上, Trying階段成功的話,confirm階段一定能成功(最終一致).
Confirm操作由TI發(fā)起,而具體的邏輯由TM控制。
3.3.1 事務(wù)管理器的confirm操作
首先事務(wù)管理器根據(jù)gtxId得到全局事務(wù)記錄以及子事務(wù)記錄集合(gtx_steps)。
按照子事務(wù)的seq從小到大的順序,依次調(diào)用子事務(wù)的confirm方法。(這個過程可以使用異步的方式并發(fā)去confirm?)
最后根據(jù)結(jié)果更新全局事務(wù)以及子事務(wù)的狀態(tài)。
只有全部子事務(wù)的狀態(tài)為完成,全局事務(wù)狀態(tài)才能更新為完成。
TI發(fā)起confirm操作后,不管本次confirm操作是否成功, 都返回成功給client。
3.4、cancel階段
對應(yīng)序列圖的No.24~43
本階段跟confirm階段邏輯類似,但是子事務(wù)的執(zhí)行順序相反。
TI發(fā)起cancel操作后,不管本次cancel操作是否成功, 都返回失敗給client。
3.5、confirm/cancel階段的異常處理
TM通過定時器,定時掃描全局事務(wù)日志表中狀態(tài)為非完成的記錄(1分鐘前),再次執(zhí)行confirm/cancel操作。
4. 業(yè)務(wù)場景
TCC場景:
4.1. 客戶端調(diào)用單獨的TCC服務(wù)

4.1.1 正常流程
try成功,confirm成功
- try階段:
1.1 t_gtx, t_gtx_step插入事務(wù)日志成功, 狀態(tài)皆為新建
1.2 tccServiceA本地事務(wù)成功
1.3 t_gtx, t_gtx_step更新事務(wù)日志成功,狀態(tài)皆為成功 - confirm階段
2.1 TM調(diào)用tccServiceA成功,更新t_gtx, t_gtx_step成功,狀態(tài)為完成。
try失敗,cancel成功
- try階段:
1.1 t_gtx, t_gtx_step插入事務(wù)日志成功, 狀態(tài)皆為新建
1.2 tccServiceA本地事務(wù)失敗
1.3 t_gtx, t_gtx_step更新事務(wù)日志成功,狀態(tài)皆為失敗 - cancel階段
2.1 TM調(diào)用tccServiceA成功,更新t_gtx, t_gtx_step成功,狀態(tài)為完成。
4.1.2 異常流程
try成功,confirm階段或者cancel階段失敗
那么后續(xù)由TM定時任務(wù)繼續(xù)重試。
4.1.3 異常流程
try階段TI插入事務(wù)日志失敗(Err-Gtx-001: begin gtx error)
如果是事務(wù)發(fā)起方(本案例), 那么TI直接返回Err-Gtx-001,本次服務(wù)調(diào)用失敗。
如果是事務(wù)參與方, 那么TI直接返回Err-Gtx-001,并最終回到事務(wù)發(fā)起方,本次全局事務(wù)失敗,并對已經(jīng)有記錄的子事務(wù)做cancel操作。
因為這里缺失了分布式事務(wù)的某個子事務(wù)日志記錄,TM無法進行confirm或者cancel操作。
try階段本地事務(wù)成功,但是TI更新事務(wù)日志失敗(Err-Gtx-002: update gtx error),子事務(wù)的狀態(tài)停留在新建的狀態(tài)
這時候如果是事務(wù)發(fā)起方(本案例),那么TI會繼續(xù)走confirm或者cancel的流程。
如果是事務(wù)參與方,把Err-Gtx-002返回, 事務(wù)發(fā)起方會忽略該錯誤,其對應(yīng)的TI會繼續(xù)走confirm或者cancel的流程。
在confirm或者cancel的邏輯里,TM會把gtxId以及該子事務(wù)id、狀態(tài)通過cookie傳過來。
如果子事務(wù)狀態(tài)為成功或者失敗,那么直接執(zhí)行confirm或者cancel邏輯;
如果子事務(wù)狀態(tài)為新建,那么目前尚不清楚到底try階段的本地事務(wù)執(zhí)行了沒。
如果執(zhí)行了, 那么必然可以通過gtxId,stepId找到在try階段的本地事務(wù)操作過的本地事務(wù)流水記錄,從而確認try階段的本地事務(wù)提交情況,再進而決定本次confirm或者cancel該做的操作。
舉個例子, 庫存服務(wù)的扣減庫存接口。
在try階段,本地事務(wù)成功,然后TI在更新子事務(wù)狀態(tài)的時候失敗了,那么該子事務(wù)狀態(tài)為新建。
然后事務(wù)發(fā)起方依然決定做confirm操作,同時庫存服務(wù)扣減庫存接口的confirm方法,通過gtxId以及stepId,找到了本地事務(wù)流水記錄,從而可以執(zhí)行confirm操作。
如果在try階段,本地事務(wù)失敗,然后TI在更新子事務(wù)狀態(tài)的時候也失敗了,那么該子事務(wù)狀態(tài)為新建。
然后事務(wù)發(fā)起方依然決定做confirm操作,同時庫存服務(wù)扣減庫存接口的confirm方法,通過gtxId以及stepId,這時候是找不到本地事務(wù)流水記錄的,說明try階段本地事務(wù)失敗。 那么業(yè)務(wù)可以調(diào)用一下把try以及confirm的邏輯合并起來,完成本次confirm操作。
4.2. 客戶端先后調(diào)用2個TCC服務(wù)

這時候, 這兩次服務(wù)調(diào)用分別構(gòu)成一個全局事務(wù), 是兩個互不相關(guān)的全局事務(wù)
4.3. 客戶端調(diào)用TCC服務(wù)a,服務(wù)a再調(diào)用TCC服務(wù)b

4.4. 客戶端調(diào)用TCC服務(wù)a,服務(wù)a再分別調(diào)用TCC服務(wù)b以及TCC服務(wù)c

4.5. 客戶端調(diào)用TCC服務(wù)a,服務(wù)a調(diào)用TCC服務(wù)b,服務(wù)b再調(diào)用TCC服務(wù)c

5. 異常流程處理
在4.3的業(yè)務(wù)場景中, tccServiceA調(diào)用tccServiceB失敗,