分布式事物解决方案-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。

 

相关文章
相关标签/搜索