分布式框架下,如何保证事物一致性一直是一个热门话题。当然事物一致性解决方案有很多种(请参考:分布式事物一致性设计思路),我们今天主要介绍TCC方案解决的思路。以下是参与设计讨论的一种解决思路,大家有问题请留言。

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="", asyncCC="true") 该注解有3个可选参数, 其中, confirm代表该接口的confirm方法名字,cancel代表该接口的cancel方法名字,asyncCC代表CC阶段是否采用异步方式。

默认情况下,methodA的confirm方法名为methodA_confirm, cancel方法名为methodA_cancel, asyncCC默认为true

2、数据表结构

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。

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(params) 全局事务开启失败的话, 返回Err-Gtx-001: Begin gtx err。

gtxId通过TransactionContext传过去(如果存在的话), params可直接用bytes

3.2.2、事务管理器处理订单服务请求

对应时序图的No.3/4/5

事务管理器根据TransactionContext是否含有gtxId去决定调用方是事务发起者还是事务参与者。 这里,orderService是事务发起方, 那么: 1、TM首先生成全局唯一的gtxId,通过createGTX(gtxId)方法创建一个全局事务(插入一条全局事务记录到t_gtx表中,状态为新建) 2、通过createStep(txId, params)方法创建一个子事务日志(插入一条子事务记录到t_gtx_step表中, 状态为新建)

全局事务开启, 操作成功后返回(gtxId, 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'

如果本子事务在加入全局事务时失败, 那么由调用端决定是否继续执行全局事务。 如果继续执行全局事务的其它子事务, 那么后续在CC阶段,本子事务将不会confirm或者cancel

TimeOut怎么办 建议事务发起者做cancel处理。

3.2.5、事务管理器处理库存服务请求

对应时序图的No.9/10

事务管理器通过gtxId发现全局事务已经开启,那么该请求来自事务参与方而不是发起方。 这时候,直接通过createStep插入一条子事务日志到t_gtx_step表中即可,并返回(gtxId,stepId)。

3.2.6、库存服务本地逻辑处理

对应时序图的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.7、订单服务本地业务逻辑处理

对应时序图的No.14/15/16

订单服务根据库存扣减的结果,决定是继续往前走还是失败回退。

如果继续往前走的话,就完成本地事务后返回结果给订单服务的TI; 如果失败回退的话,就把失败信息返回给订单服务的TI。

至此,Trying阶段完成。

根据本阶段的结果, TI将会进入TCC的confirm(成功)或者cancel阶段(失败)

3.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.4、cancel阶段

对应序列图的No.31~44 本阶段跟confirm阶段逻辑类似,但是子事务的执行顺序相反。

TI发起cancel操作后,不管本次cancel操作是否成功, 都返回失败给client。

3.5、confirm/cancel阶段的异常处理

TM通过定时器,定时扫描全局事务日志表中状态为非完成的记录(5分钟前),再次执行confirm/cancel操作。

4. 业务场景

TCC场景:

4.1. 客户端调用单独的TCC服务

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服务

这时候, 这两次服务调用分别构成一个全局事务, 是两个互不相关的全局事务

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

问题

定时器发起的全局事务, 不经过TI。。。

定时器可通过客户端的方式调用服务,而不是直接调用action。

 请关注优秀的开源自研分布式框架:Dapeng
 
原文地址:TCC support

分布式事物解决方案-TCC的更多相关文章

  1. Atomikos和GTS-Fescar和TCC-Transaction和TX-LCN分布式事物的比较

    什么是分布式事物 分布式系统中保证不同节点之间的数据一致性的事物,叫做分布式事物. 为什么要用分布式事物 微服务,SOA等服务架构模式,一个是service产生多个节点,另一个是resource产生多 ...

  2. 分布式事务解决方案以及 .Net Core 下的实现(上)

    数据一致性是构建业务系统需要考虑的重要问题 , 以往我们是依靠数据库来保证数据的一致性.但是在微服务架构以及分布式环境下实现数据一致性是一个很有挑战的的问题.最近在研究分布式事物,分布式的解决方案有很 ...

  3. 分布式事务解决方案汇总:2PC、3PC、消息中间件、TCC、状态机+重试+幂等(转)

    数据一致性问题非常多样,下面举一些常见例子.比如在更新数据的时候,先更新了数据库,后更新了缓存,一旦缓存更新失败,此时数据库和缓存数据会不一致.反过来,如果先更新缓存,再更新数据库,一旦缓存更新成功, ...

  4. 2018-01-08 学习随笔 SpirngBoot整合Mybatis进行主从数据库的动态切换,以及一些数据库层面和分布式事物的解决方案

    先大概介绍一下主从数据库是什么?其实就是两个或N个数据库,一个或几个主负责写(当然也可以读),另一个或几个从只负责读.从数据库要记录主数据库的具体url以及BigLOG(二进制日志文件)的参数.原理就 ...

  5. 分布式事务解决方案,中间件 Seata 的设计原理详解

    作者:张乘辉 前言 在微服务架构体系下,我们可以按照业务模块分层设计,单独部署,减轻了服务部署压力,也解耦了业务的耦合,避免了应用逐渐变成一个庞然怪物,从而可以轻松扩展,在某些服务出现故障时也不会影响 ...

  6. SpringCloudAlibaba分布式事务解决方案Seata实战与源码分析-上

    概述 定义 Spring Cloud Alibaba Seata 官网地址 https://seata.io/zh-cn/ 最新版本1.5.2 Spring Cloud Alibaba Seata 文 ...

  7. 阿里开源分布式事务解决方案 Fescar

    微服务倡导将复杂的单体应用拆分为若干个功能简单.松耦合的服务,这样可以降低开发难度.增强扩展性.便于敏捷开发.当前被越来越多的开发者推崇,系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并 ...

  8. 阿里微服务架构下分布式事务解决方案-GTS

    虽然微服务现在如火如荼,但对其实践其实仍处于初级阶段.即使互联网巨头的实践也大多是试验层面,鲜有核心业务系统微服务化的案例.GTS是目前业界第一款,也是唯一的一款通用的解决微服务分布式事务问题的中间件 ...

  9. 分布式事务解决方案FESCAR

    项目地址:FESCAR 以下是官网的文档.简介2019年,Fescar 是 阿里巴巴 开源的 分布式事务中间件,以 高效 并且对业务 0 侵入 的方式,解决 微服务 场景下面临的分布式事务问题. 1. ...

随机推荐

  1. 单独KafkaConsumer实例and多worker线程。

    1.单独KafkaConsumer实例and多worker线程.将获取的消息和消息的处理解耦,将消息的处理放入单独的工作者线程中,即工作线程中,同时维护一个或者若各干consumer实例执行消息获取任 ...

  2. WebGIS之MapBox篇

    前面在Arcgis的基础上玩了玩,这不最近又去摸索了一下Web上开源的GIS;这次选择了基于MapBox来实现一些效果: 1.加载自己发布的本地瓦片效果 2.加载热力图.Echarts.三位建筑.路况 ...

  3. VSCode搭建django项目

    之前我们使用VSCode搭建C#项目,今天写一篇关于django项目的搭建,其实以其说是搭建django框架,不如说是如何通过vscode开发django项目:django官网:https://www ...

  4. SpringBoot+Swagger整合

    0.引言及注意事项 Swagger是一个接口文档工具,依照Swagger可以0配置开发接口.不过要注意,Swagger是基于SpringBoot1.47版本开发的,而SpringBoot现在基本都是是 ...

  5. 微信小程序的线程架构

    小程序的线程架构 每个小程序包含一个描述整体程序的app实例和多个描述页面的page. 其中app由3个文件构成: app.json 公共配置文件 app.wxss 公共样式文件 app.js 主体逻 ...

  6. 【已采纳】新项目第一次怎么上传到github里面

      言归正传,最近学习了怎么将新创建的本地代码上传到github上,这里简单的记录一下,我喜欢使用命令行,这里全用命令行来实现,不了解git命令的可以去了解下. 第一步:建立git仓库 cd到你的本地 ...

  7. modbus_tk模块

    modbus_tk模块 通过modbus-RTU 读取地址,调用后返回反馈数值和故障信息. modbus_tk模块安装 pip install pymodbus_tk 下面代码功能:读取地址为0x42 ...

  8. 001-OpenStack-基础环境

    OpenStack-基础环境 1.实验描述 通过搭建 OpenStack 的 ocata 版,来学习虚拟化技术 2.实验环境 [你可能需要][CentOS 7 搭建模板机]点我快速打开文章 [你可能需 ...

  9. 07.进程管理+作业控制+文件查找与压缩+文件压缩与打包+tar打包解包+NFS

    进程管理 程序放在磁盘上叫文件,把它复制到内存,并在cpu运行,就叫进程, 进程多少也反映当前运行程序的多少 进程在系统中会为每个进程生成一个进程号,在所有的进程中有一个特殊进程即init进程, 它是 ...

  10. 201871010128-杨丽霞《面向对象程序设计(java)》第十六周学习总结

    201871010128-杨丽霞<面向对象程序设计(java)>第十六周学习总结(1分) 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-dai ...