分佈式事物解決方案-TCC

  分佈式框架下,如何保證事物一致性一直是一個熱門話題。固然事物一致性解決方案有不少種(請參考:分佈式事物一致性設計思路),咱們今天主要介紹TCC方案解決的思路。如下是參與設計討論的一種解決思路,你們有問題請留言。

一、基本概念

TI:Transaction Interceptor,事務攔截器,位於dapeng容器的filterChain鏈中。html

因爲TI的邏輯會比較複雜, 不太適合在IO線程中操做git

TM:Transaction Manager, 事務管理器,做爲一個獨立的服務存在。github

事務發起方: 服務調用鏈或者說請求會話中第一個加入全局事務的接口方法,稱爲事務發起方。markdown

事務參與方: 服務調用鏈或者說請求會話中除事務發起方的其它加入了全局事務的接口方法,稱爲事務參與方。app

例如,對於服務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操做。async

事務管理器負責confirm或者cancel失敗後的重試。分佈式

在定義接口的時候, 須要加上如下註解,以代表該接口須要加入全局事務。@TCC(confirm="",cancel="", asyncCC="true") 該註解有3個可選參數, 其中, confirm表明該接口的confirm方法名字,cancel表明該接口的cancel方法名字,asyncCC表明CC階段是否採用異步方式。post

默認狀況下,methodA的confirm方法名爲methodA_confirm, cancel方法名爲methodA_cancel, asyncCC默認爲true

二、數據表結構

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。

三、案例描述

這裏以訂單建立爲例。

用戶建立訂單,同時扣除庫存。

其中訂單、庫存分別爲兩個不一樣的服務。同時, TM也是一個單獨的服務。

本流程有2個業務服務參與,分別是訂單服務的建立訂單接口以及庫存服務的庫存扣減接口。

業務主流程以下:

一、客戶端調用orderService.createOrder, 發起訂單建立流程
二、orderService調用stockService.decreaseStock, 扣減庫存
三、orderService建立訂單,並返回客戶端。

對應的訂單建立序列圖以下: 建立訂單

3.1. 客戶端發起訂單建立的操做

對應時序圖的No.1調用

參數

3.二、全局事務的Try階段

訂單服務的全局事務攔截器(TI)收到請求後, 識別到目標方法帶有TCC標識,即進入Trying階段。

3.2.一、訂單服務開啓全局事務

TI向事務管理服務請求開啓全局事務,對應時序圖的No.2。 tm.beginGTX(params) 全局事務開啓失敗的話, 返回Err-Gtx-001: Begin gtx err。

gtxId經過TransactionContext傳過去(若是存在的話), params可直接用bytes

3.2.二、事務管理器處理訂單服務請求

對應時序圖的No.3/4/5

事務管理器根據TransactionContext是否含有gtxId去決定調用方是事務發起者仍是事務參與者。 這裏,orderService是事務發起方, 那麼: 一、TM首先生成全局惟一的gtxId,經過createGTX(gtxId)方法建立一個全局事務(插入一條全局事務記錄到t_gtx表中,狀態爲新建) 二、經過createStep(txId, params)方法建立一個子事務日誌(插入一條子事務記錄到t_gtx_step表中, 狀態爲新建)

全局事務開啓, 操做成功後返回(gtxId, stepId),繼續下一步,不然失敗後直接返回調用方,由調用方決定是繼續仍是回滾(在這個案例中, 這裏的調用方是client)。

3.2.三、訂單服務的TI轉發請求到具體的業務服務方法

對應時序圖中的No.6/7 全局事務開啓成功後, TI轉發請求到業務服務。這裏爲orderService.createOrder

在這個方法中, 首先調用庫存服務的扣減庫存接口:stockService.decreaseStock

若是全局事務開啓失敗,那麼TI會直接報錯返回給調用方(Err-Gtx-001: begin gtx error)

3.2.四、庫存服務開啓全局事務

對應時序圖的No.8

同3.2.1,庫存服務的TI收到扣減庫存請求後,開啓全局事務: `tm.beginGTX'

若是本子事務在加入全局事務時失敗, 那麼由調用端決定是否繼續執行全局事務。 若是繼續執行全局事務的其它子事務, 那麼後續在CC階段,本子事務將不會confirm或者cancel

TimeOut怎麼辦 建議事務發起者作cancel處理。

3.2.五、事務管理器處理庫存服務請求

對應時序圖的No.9/10

事務管理器經過gtxId發現全局事務已經開啓,那麼該請求來自事務參與方而不是發起方。 這時候,直接經過createStep插入一條子事務日誌到t_gtx_step表中便可,並返回(gtxId,stepId)。

3.2.六、庫存服務本地邏輯處理

對應時序圖的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.七、訂單服務本地業務邏輯處理

對應時序圖的No.14/15/16

訂單服務根據庫存扣減的結果,決定是繼續往前走仍是失敗回退。

若是繼續往前走的話,就完成本地事務後返回結果給訂單服務的TI; 若是失敗回退的話,就把失敗信息返回給訂單服務的TI。

至此,Trying階段完成。

根據本階段的結果, TI將會進入TCC的confirm(成功)或者cancel階段(失敗)

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.四、cancel階段

對應序列圖的No.31~44 本階段跟confirm階段邏輯相似,可是子事務的執行順序相反。

TI發起cancel操做後,無論本次cancel操做是否成功, 都返回失敗給client。

3.五、confirm/cancel階段的異常處理

TM經過定時器,定時掃描全局事務日誌表中狀態爲非完成的記錄(5分鐘前),再次執行confirm/cancel操做。

4. 業務場景

TCC場景:

4.1. 客戶端調用單獨的TCC服務

image.png

4.1.1 正常流程

try成功,confirm成功

  1. try階段:
1.1 t_gtx, t_gtx_step插入事務日誌成功, 狀態皆爲新建
  1.2 tccServiceA本地事務成功
  1. confirm階段 2.1 TM調用tccServiceA成功,更新t_gtx, t_gtx_step成功,狀態爲完成。

try失敗,cancel成功

  1. try階段:
1.1 t_gtx, t_gtx_step插入事務日誌成功, 狀態皆爲新建
  1.2 tccServiceA本地事務失敗
  1. 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服務

image.png

這時候, 這兩次服務調用分別構成一個全局事務, 是兩個互不相關的全局事務

4.3. 客戶端調用TCC服務a,服務a再調用TCC服務b

image.png

4.4. 客戶端調用TCC服務a,服務a再分別調用TCC服務b以及TCC服務c

image.png

4.5. 客戶端調用TCC服務a,服務a調用TCC服務b,服務b再調用TCC服務c

image.png

問題

定時器發起的全局事務, 不通過TI。。。

定時器可經過客戶端的方式調用服務,而不是直接調用action。

 

相關文章
相關標籤/搜索