1、基本概念
TI:Transaction Interceptor,事务拦截器,位于dapeng容器的filterChain链中。
由于TI的逻辑会比较复杂, 不太适合在IO线程中操作
TM:Transaction Manager, 事务管理器,作为一个独立的服务存在。
事务发起方: 服务调用链或者说请求会话中第一个加入全局事务的接口方法,称为事务发起方。
事务参与方: 服务调用链或者说请求会话中除事务发起方的其它加入了全局事务的接口方法,称为事务参与方。
例如,对于服务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操作。
事务管理器负责confirm或者cancel失败后的重试。
在定义接口的时候, 需要加上以下注解,以表明该接口需要加入全局事务。
@TCC(confirm="",cancel="")
该注解有2个可选参数, 其中, confirm代表该接口的confirm方法名字,cancel代表该接口的cancel方法名字。
默认情况下,methodA的confirm方法名为methodA_confirm, cancel方法名为methodA_cancel
2、数据表结构
t_gtx
CREATE TABLE IF NOT EXISTS `gtx_db`.`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)',
`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) 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
对于参与分布式事务的服务接口,需要在本地有个事务流水表(例如orderDb):
CREATE TABLE IF NOT EXISTS `order_db`.`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',
`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 */ /*水表。 当本地事务成功时, 由本地业务*/
3、案例描述
这里以订单创建为例。
用户创建订单,同时扣除库存。
其中订单、库存分别为两个不同的服务。同时, TM也是一个单独的服务。
本流程有2个业务服务参与,分别是订单服务的创建订单接口以及库存服务的库存扣减接口。
业务主流程如下:
1、客户端调用orderService.createOrder, 发起订单创建流程
2、orderService调用stockService.decreaseStock, 扣减库存
3、orderService创建订单,并返回客户端。
对应的订单创建序列图如下:
3.1. 客户端发起订单创建的操作
对应时序图的No.1调用
参数
3.2、全局事务的Try阶段
订单服务的全局事务拦截器(TI)收到请求后, 识别到目标方法带有TCC标识,即进入Trying
阶段。
3.2.1、订单服务开启全局事务
TI向事务管理服务请求开启全局事务,对应时序图的No.2。
tm.beginGTX(gtxId, params)
txId可用sessionTid(long的形式),params可直接用bytes
3.2.2、事务管理器处理订单服务请求
对应时序图的No.3/4/5
事务管理器根据txId去决定调用方是事务发起者还是事务参与者。
这里,orderService是事务发起方, 那么:
1、TM首先通过createTGX(txId)方法创建一个全局事务(插入一条全局事务记录到t_gtx表中,状态为新建)
2、通过createStep(txId, params)方法创建一个子事务日志(插入一条子事务记录到t_gtx_step表中, 状态为新建)
全局事务开启, 操作成功后返回stepId继续下一步,否则失败后直接返回调用方,由调用方决定是继续还是回滚(在这个案例中, 这里的调用方是client)。
3.2.3、订单服务的TI转发请求到具体的业务服务方法
对应时序图中的No.6/7
全局事务开启成功后, TI转发请求到业务服务。这里为orderService.createOrder
。
在这个方法中, 首先调用库存服务的扣减库存接口:stockService.decreaseStock
如果全局事务开启失败,那么TI会直接报错返回给调用方(Err-Gtx-001: begin gtx error)
3.2.4、库存服务开启全局事务
对应时序图的No.8
同3.2.1,库存服务的TI收到扣减库存请求后,开启全局事务: `tm.beginGTX'
3.2.5、事务管理器处理库存服务请求
对应时序图的No.9/10
事务管理器通过gtxId发现全局事务已经开启,那么该请求来自事务参与方而不是发起方。
这时候,直接通过createStep
插入一条子事务日志到t_gtx_step表中即可,并返回stepId。
3.2.6、库存服务本地逻辑处理
对应时序图的No.11/12/13
TI开始全局事务成功后, 转发扣减库存请求给具体的业务方法。
库存服务执行本地事务(库存余额扣减,冻结库存增加)后返回到TI
3.2.7、库存服务的TI更新全局事务
对应时序图的No.14/15/16
TI根据3.2.6的结果,调用tm.updateGTX
更新全局事务。
TM根据gtxId以及stepId判断该请求来自事务参与方,那么仅更新子事务日志表updateStep
, 状态为成功/失败。
这一步有可能失败,导致本地子事务提交后,结果没反映到TM的子事务表的状态中。
还有一个可能就是本地子事务成功,TI更新全局事务也成功了, 但是由于网络中断或者其他原因,导致服务调用方(这里是orderService)的对扣减库存调用失败。
不管如何,服务调用方调用失败后,由服务调用方自行决定是继续前行还是回滚全局事务。
3.2.8、订单服务本地业务逻辑处理
对应时序图的No.18/19
订单服务根据库存扣减的结果,决定是继续往前走还是失败回退。
如果继续往前走的话,就完成本地事务后返回结果给订单服务的TI;
如果失败回退的话,就把失败信息返回给订单服务的TI。
3.2.9、订单服务的TI更新全局事务
对应序列图的No.20/21/22/23
如果订单服务本地事务成功,那么TI通过tm.updateGTX
把结果反馈给TM。
TM根据gtxId
判断该请求来自事务发起方,那么根据status把全局事务状态更新为成功/失败;
同时, 更新子事务状态为成功/失败
全局事务的最终状态跟事务发起方对应的子事务的最终状态一致。
至此,Trying阶段完成。
根据本阶段的结果, TI将会进入TCC的confirm
(成功)或者cancel
阶段(失败)
3.3、confirm阶段
对应序列图的No.24~33
理论上, Trying阶段成功的话,confirm阶段一定能成功(最终一致).
Confirm操作由TI发起,而具体的逻辑由TM控制。
3.3.1 事务管理器的confirm操作
首先事务管理器根据gtxId
得到全局事务记录以及子事务记录集合(gtx_steps
)。
按照子事务的seq从小到大的顺序,依次调用子事务的confirm方法。(这个过程可以使用异步的方式并发去confirm?)
最后根据结果更新全局事务以及子事务的状态。
只有全部子事务的状态为完成,全局事务状态才能更新为完成。
TI发起confirm操作后,不管本次confirm操作是否成功, 都返回成功给client。
3.4、cancel阶段
对应序列图的No.24~43
本阶段跟confirm阶段逻辑类似,但是子事务的执行顺序相反。
TI发起cancel操作后,不管本次cancel操作是否成功, 都返回失败给client。
3.5、confirm/cancel阶段的异常处理
TM通过定时器,定时扫描全局事务日志表中状态为非完成的记录(1分钟前),再次执行confirm/cancel操作。
4. 业务场景
TCC场景:
4.1. 客户端调用单独的TCC服务
4.1.1 正常流程
try成功,confirm成功
- try阶段:
1.1 t_gtx, t_gtx_step插入事务日志成功, 状态皆为新建
1.2 tccServiceA本地事务成功
1.3 t_gtx, t_gtx_step更新事务日志成功,状态皆为成功 - confirm阶段
2.1 TM调用tccServiceA成功,更新t_gtx, t_gtx_step成功,状态为完成。
try失败,cancel成功
- try阶段:
1.1 t_gtx, t_gtx_step插入事务日志成功, 状态皆为新建
1.2 tccServiceA本地事务失败
1.3 t_gtx, t_gtx_step更新事务日志成功,状态皆为失败 - 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操作。
因为这里缺失了分布式事务的某个子事务日志记录,TM无法进行confirm或者cancel操作。
try阶段本地事务成功,但是TI更新事务日志失败(Err-Gtx-002: update gtx error),子事务的状态停留在新建的状态
这时候如果是事务发起方(本案例),那么TI会继续走confirm或者cancel的流程。
如果是事务参与方,把Err-Gtx-002返回, 事务发起方会忽略该错误,其对应的TI会继续走confirm或者cancel的流程。
在confirm或者cancel的逻辑里,TM会把gtxId以及该子事务id、状态通过cookie传过来。
如果子事务状态为成功或者失败,那么直接执行confirm或者cancel逻辑;
如果子事务状态为新建,那么目前尚不清楚到底try阶段的本地事务执行了没。
如果执行了, 那么必然可以通过gtxId,stepId找到在try阶段的本地事务操作过的本地事务流水记录,从而确认try阶段的本地事务提交情况,再进而决定本次confirm或者cancel该做的操作。
举个例子, 库存服务的扣减库存接口。
在try阶段,本地事务成功,然后TI在更新子事务状态的时候失败了,那么该子事务状态为新建。
然后事务发起方依然决定做confirm操作,同时库存服务扣减库存接口的confirm方法,通过gtxId以及stepId,找到了本地事务流水记录,从而可以执行confirm操作。
如果在try阶段,本地事务失败,然后TI在更新子事务状态的时候也失败了,那么该子事务状态为新建。
然后事务发起方依然决定做confirm操作,同时库存服务扣减库存接口的confirm方法,通过gtxId以及stepId,这时候是找不到本地事务流水记录的,说明try阶段本地事务失败。 那么业务可以调用一下把try以及confirm的逻辑合并起来,完成本次confirm操作。
4.2. 客户端先后调用2个TCC服务
这时候, 这两次服务调用分别构成一个全局事务, 是两个互不相关的全局事务
4.3. 客户端调用TCC服务a,服务a再调用TCC服务b
4.4. 客户端调用TCC服务a,服务a再分别调用TCC服务b以及TCC服务c
4.5. 客户端调用TCC服务a,服务a调用TCC服务b,服务b再调用TCC服务c
5. 异常流程处理
在4.3的业务场景中, tccServiceA调用tccServiceB失败,