1羡疗、基本概念
TI:Transaction Interceptor,事務(wù)攔截器别洪,位于dapeng容器的filterChain鏈中叨恨。
由于TI的邏輯會比較復(fù)雜, 不太適合在IO線程中操作
TM:Transaction Manager, 事務(wù)管理器挖垛,作為一個獨(dú)立的服務(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ù)管理器負(fù)責(zé)confirm或者cancel失敗后的重試义屏。
在定義接口的時候靠汁, 需要加上以下注解,以表明該接口需要加入全局事務(wù)闽铐。
@TCC(confirm="",cancel="")
該注解有2個可選參數(shù)蝶怔, 其中, confirm代表該接口的confirm方法名字兄墅,cancel代表該接口的cancel方法名字踢星。
默認(rèn)情況下,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也是一個單獨(dú)的服務(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)收到請求后珠十, 識別到目標(biāo)方法帶有TCC標(biāo)識料扰,即進(jìn)入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將會進(jìn)入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)用單獨(dú)的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無法進(jìn)行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ù)流水記錄,從而確認(rèn)try階段的本地事務(wù)提交情況阴汇,再進(jìn)而決定本次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失敗秋度,