dapeng-soa分布式事務(wù)設(shè)計

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)建序列圖如下:


創(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ù)

image.png

4.1.1 正常流程

try成功,confirm成功

  1. 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)皆為成功
  2. confirm階段
    2.1 TM調(diào)用tccServiceA成功,更新t_gtx, t_gtx_step成功,狀態(tài)為完成。

try失敗,cancel成功

  1. 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)皆為失敗
  2. 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ù)

image.png

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

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

image.png

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

image.png

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

image.png

5. 異常流程處理

在4.3的業(yè)務(wù)場景中, tccServiceA調(diào)用tccServiceB失敗,

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一個TCC事務(wù)框架需要解決的當(dāng)然是分布式事務(wù)的管理。關(guān)于TCC事務(wù)機制的介紹,可以參考TCC事務(wù)機制簡介。 TCC...
    Java高級進階閱讀 1,547評論 0 0
  • 一. 事務(wù) 1.1 什么是事務(wù) 數(shù)據(jù)庫事務(wù)(簡稱:事務(wù),Transaction)是指數(shù)據(jù)庫執(zhí)行過程中的一個邏輯單位...
    Java小鋪閱讀 638評論 0 3
  • 為什么無眠?想要把夜拉長,再拉長一些! 夜是寫滿悲歡的月色,伴隨著不愿迷失的靈魂,怕自己睡下貪戀夢的旖旎! 夜是掛...
    馨欣然閱讀 429評論 0 3
  • 時間習(xí)慣書寫故事 在每個地方珍藏一段記憶 然后凝化于心 標簽: 原創(chuàng)
    趙藝閎Z閱讀 170評論 0 0

友情鏈接更多精彩內(nèi)容