首页 > 其他分享 >Seata TCC模式实战

Seata TCC模式实战

时间:2022-12-07 20:04:43浏览次数:99  
标签:实战 事务 seata spring order TCC cloud Seata


前言

最近状态有点不好,所以创作动力不足,发觉日常生活一定要做减法,对少量的事保持持续专注的投入,养成良好的习惯。
今天补充下,Seata TCC模式实战。


一、TCC设计原则

从 TCC 模型的框架可以发现,TCC 模型的核心在于 TCC 接口的设计。用户在接入 TCC 时,大部分工作都集中在如何实现 TCC 服务上。

设计一套 TCC 接口最重要的是什么?主要有两点,
第一点,需要将操作分成两阶段完成。TCC(Try-Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。

TCC 模型认为对于业务系统中一个特定的业务逻辑 ,其对外提供服务时,必须接受一些不确定性,即对业务逻辑初步操作的调用仅是一个临时性操作,调用它的主业务服务保留了后续的取消权。如果主业务服务认为全局事务应该回滚,它会要求取消之前的临时性操作,这就对应从业务服务的取消操作。而当主业务服务认为全局事务应该提交时,它会放弃之前临时性操作的取消权,这对应从业务服务的确认操作。每一个初步操作,最终都会被确认或取消。

因此,针对一个具体的业务服务,TCC 分布式事务模型需要业务系统提供三段业务逻辑

1、初步操作 Try:完成所有业务检查,预留必须的业务资源。
2、确认操作 Confirm:真正执行的业务逻辑,不做任何业务检查,只使用Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功另外,Confirm操作需满足幂等性,保证一笔分布式事务能且只能成功一次。
注意:这里说的confirm方法必须能成功是指满足业务执行的条件,比如有足够的预留资源扣减,方法本身不能说保证一定成功。因为所有的网络操作都具有不确定性。
3、取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel操作也需要满足幂等性。

第二点,就是要根据自身的业务模型控制并发,这个对应 ACID 中的隔离性。后面会详细讲到。

二、TCC执行逻辑图

Seata TCC模式实战_TCC

三、TCC模式和AT模式对比

根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持
一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
二阶段 commit 行为:调用 自定义 的 commit 逻辑。
二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

综合比较:
1、AT需要支持本地 ACID 事务的关系型数据库,TCC不依赖底层数据资源对事务的支持。
2、AT模式使用了全局锁和本地锁,能保证强一致性。TCC只能保证最终一致性。
3、AT模式由于大量使用了锁,吞吐量较低。
4、实现复杂度上,AT模式只需要简单的添加全局事务注解,基本可以实现零侵入,而TCC模式需要自己实现prepare、commit、rollback方法,并考虑空回滚、幂等、悬挂等问题,实现成本较高。

四、TCC三大异常说明

最常见的主要是这三种异常,分别是空回滚、幂等、悬挂。

1、空回滚

首先是空回滚。什么是空回滚?空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。

什么样的情形会造成空回滚呢?可以看图中的第 2 步,前面讲过,注册分支事务是在调用 RPC 时,Seata 框架的切面会拦截到该次调用请求,先向 TC 注册一个分支事务,然后才去执行 RPC 调用逻辑。如果 RPC 调用逻辑有问题,比如调用方机器宕机、网络异常,都会造成 RPC 调用失败,即未执行 Try 方法。但是分布式事务已经开启了,需要推进到终态,因此,TC 会回调参与者二阶段 Cancel 接口,从而形成空回滚。

Seata TCC模式实战_回滚_02


那会不会有空提交呢?理论上来说不会的,如果调用方宕机,那分布式事务默认是回滚的。如果是网络异常,那 RPC 调用失败,发起方应该通知 TC 回滚分布式事务,这里可以看出为什么是理论上的,就是说发起方可以在 RPC 调用失败的情况下依然通知 TC 提交,这时就会发生空提交,这种情况要么是编码问题,要么开发同学明确知道需要这样做。

那怎么解决空回滚呢?前面提到,Cancel 要识别出空回滚,直接返回成功。那关键就是要识别出这个空回滚。**思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回滚;**如果没执行,那就是空回滚。因此,需要一张额外的事务控制表,其中有分布式事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。

2、幂等

接下来是幂等。幂等就是对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,因此,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致资损等严重问题。

什么样的情形会造成重复提交或回滚?从图中可以看到,提交或回滚是一次 TC 到参与者的网络调用。因此,网络故障、参与者宕机等都有可能造成参与者 TCC 资源实际执行了二阶段防范,但是 TC 没有收到返回结果的情况,这时,TC 就会重复调用,直至调用成功,整个分布式事务结束。

Seata TCC模式实战_回滚_03


怎么解决重复执行的幂等问题呢?一个简单的思路就是记录每个分支事务的执行状态。在执行前状态,如果已执行,那就不再执行;否则,正常执行。前面在讲空回滚的时候,已经有一张事务控制表了,事务控制表的每条记录关联一个分支事务,那我们完全可以在这张事务控制表上加一个状态字段,用来记录每个分支事务的执行状态。

Seata TCC模式实战_TCC_04


如图所示,该状态字段有三个值,分别是初始化、已提交、已回滚。Try 方法插入时,是初始化状态。二阶段 Confirm 和 Cancel 方法执行后修改为已提交或已回滚状态。当重复调用二阶段接口时,先获取该事务控制表对应记录,检查状态,如果已执行,则直接返回成功;否则正常执行。

3、悬挂

最后是防悬挂。悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。因为允许空回滚的原因,Cancel 接口认为 Try 接口没执行,空回滚直接返回成功,对于 Seata 框架来说,认为分布式事务的二阶段接口已经执行成功,整个分布式事务就结束了。但是这之后 Try 方法才真正开始执行,预留业务资源,前面提到事务并发控制的业务加锁,对于一个 Try 方法预留的业务资源,只有该分布式事务才能使用,然而 Seata 框架认为该分布式事务已经结束,也就是说,当出现这种情况时,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。

简单来说,悬挂就是 Cancel 接口比 Try 接口先执行,Cancel 接口进行了空回滚,Try 接口才执行进行资源预留,而预留的资源又没有对应的Cancel 接口去进行消费,所以导致这部分预留资源没法处理。

什么样的情况会造成悬挂呢?按照前面所讲,在 RPC 调用时,先注册分支事务,再执行 RPC 调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,发起方就会通知 TC 回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者,真正执行,从而造成悬挂。

怎么实现才能做到防悬挂呢?根据悬挂出现的条件先来分析下,悬挂是指二阶段 Cancel 执行完后,一阶段才执行。也就是说,为了避免悬挂,如果二阶段执行完成,那一阶段就不能再继续执行。因此,当一阶段执行时,需要先检查二阶段是否已经执行完成,如果已经执行,则一阶段不再执行;否则可以正常执行。那怎么检查二阶段是否已经执行呢?大家是否想到了刚才解决空回滚和幂等时用到的事务控制表,可以在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,如果记录存在,就认为二阶段已经执行;否则二阶段没执行。

五、案例实战

1、业务说明

Seata TCC模式实战_TCC_05


业务说明:

1、用户向Order服务发起下订单的请求;

2、Order服务收到请求后,开始创建订单;

3、Order服务向Storage库存服务发起请求,减去商品库存;

4、Order服务向Account账户服务发起请求,减少账户余额;

5、全部执行成功,则成功创建订单。

2、项目结构

Seata TCC模式实战_回滚_06


说明:

1、account工程是账户服务,用户管理账户余额。

2、db-init工程用户初始化项目表结构和数据。

3、easy-id-generator工程用来生成全局唯一的订单id,用来控制创建订单方法的幂等性。

4、eureka-server工程是采用eureka作为注册中心。

5、order工程是订单服务,用来新建订单。

6、order-parent是整合微服务项目的公共父依赖,类似spring-boot-starter-parent。

7、storage工程是库存服务,用来管理商品库存。

3、maven依赖

spring cloud和spring boot的版本对应关系:

Seata TCC模式实战_spring_07

spring cloud、spring boot、spring cloud Alibaba(spring-cloud-starter-alibaba-seata)三者的版本对应关系。

Seata TCC模式实战_回滚_08


和seata之间的版本对应关系:​​官方地址​

Seata TCC模式实战_spring_09


注意:

spring-cloud-starter-alibaba-seata归属与Spring Cloud Alibaba体系,两者版本保持一致。

版本说明:
spring-cloud-starter-alibaba-seata 2.1.0内嵌seata-all 0.7.1,2.1.1内嵌seata-all 0.9.0,2.2.0内嵌seata-spring-boot-starter 1.0.0, 2.2.1内嵌seata-spring-boot-starter 1.1.0。

其中seata-spring-boot-starter的核心是包含一个对应的seata-all依赖,两者的版本保持一致。

引用的时候主要需要保证spring-cloud-starter-alibaba-seata和当前项目的spring cloud的版本保持一致,
seata的版本可以通过exclusion排除默认依赖后,升级成较新的依赖。这样就做到了spring cloud版本和seate版本的解耦。

spring-cloud-starter-alibaba-seata推荐依赖配置方式

<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>最新版</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.1.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>

说明:
spring-cloud-starter-alibaba-seata的版本选择与项目依赖的spring cloud相匹配的版本。通过exclusion排除默认依赖的seata,然后引入自己想要的seata版本,实现spring cloud的版本和seata的依赖版本解耦。
注意spring-cloud-starter-alibaba-seata 2.1.0内嵌seata-all 0.7.1,2.1.1内嵌seata-all 0.9.0,2.2.0内嵌seata-spring-boot-starter 1.0.0, 2.2.1内嵌seata-spring-boot-starter 1.1.0。

所以如果是spring-cloud-starter-alibaba-seata 2.1.x的版本,依赖的是seata-all ,所以推荐如下配置:

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>${spring-cloud-alibaba-seata.version}</version>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>${seata.version}</version>
</dependency>

order-parent父级项目的核心依赖:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>order-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>order-parent</name>


<properties>
<mybatis-plus.version>3.3.2</mybatis-plus.version>
<druid-spring-boot-starter.version>1.1.23</druid-spring-boot-starter.version>
<seata.version>1.3.0</seata.version>
<spring-cloud-alibaba-seata.version>2.0.0.RELEASE</spring-cloud-alibaba-seata.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
<skipTests>true</skipTests>
</properties>

<dependencies>
<!-- 打开 seata 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>${spring-cloud-alibaba-seata.version}</version>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>${seata.version}</version>
</dependency>
……
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Seata TCC模式实战_分布式事务_10

4、属性配置

(1)、conf文件方式

针对spring-cloud-starter-alibaba-seata 2.1.x的版本,由于依赖的是seata-all,没有整合更丰富的seata配置,所以一般采用的是属性文件 + conf配置文件的方式进行配置:

Seata TCC模式实战_Seata_11


属性文件配置如下:

spring.cloud.alibaba.seata.tx-service-group=order_tx_group

说明:
通过设置spring.cloud.alibaba.seata.tx-service-group属性设置事务服务的分组名称。

file.conf和registry.conf文件都可以在seata按照文件下找到。其中file.conf用来配置网络、服务端server以及客户端的相关属性。
registry.conf用来配置seata server注册的注册中心,以及属性文件保存的配置中心。

file.conf核心配置:

service {
#transaction service group mapping
# order_tx_group 与 yml 中的 “tx-service-group: order_tx_group” 配置一致
# “seata-server” 与 TC 服务器的注册名一致
# order_tx_group名称需要和属性文件中配置的spring.cloud.alibaba.seata.tx-service-group的值相匹配
# vgroupMapping属性是为了配置
vgroupMapping.order_tx_group = "seata-server"
#only support when registry.type=file, please don't set multiple addresses
# seata-server的名称需要和上面的vgroupMapping.order_tx_group的值相匹配。
seata-server.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}

注意:
1、vgroupMapping.order_tx_group中的order_tx_group需要和属性文件中配置的spring.cloud.alibaba.seata.tx-service-group的值相匹配
2、 seata-server.grouplist中的seata-server.需要和vgroupMapping.order_tx_group的值匹配
核心的目的就是指定seata-server的服务分组名称,以及seata-server服务对应的服务器ip地址。

registry.conf核心配置:
这里由于测试,且我的seata-server服务采用的默认file方式部署的,所以这里的registry.conf也都是采用的默认的,没有做相关修改。实际使用中可以根据情况和需求,修改成自己对应的配置中心和注册中心。

registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"

file {
name = "file.conf"
}
}

config {
# file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
type = "file"
file {
name = "file.conf"
}
}

(2)、属性文件配置方式

针对spring-cloud-starter-alibaba-seata 2.2.x以后的版本,由于依赖的是seata-spring-boot-starter,整合了更丰富的seata配置,所以可以省去conf文件,将seata相关数据都在属性文件中进行配置。

Seata TCC模式实战_TCC_12


application.yml属性配置:

seata:
enabled: true
tx-service-group: order_tx_group
service:
vgroup-mapping:
order_tx_group: seata-server
grouplist:
seata-server: 127.0.0.1:8091

注意:

1、注意属性文件配置中map类型属性配置的对应关系。

2、注意几个属性之间的关联关系。

Seata TCC模式实战_分布式事务_13

(3)、补充说明

事务分组说明

  1. 事务分组是什么?
    事务分组是seata的资源逻辑,类似于服务实例。在file.conf中的my_test_tx_group就是一个事务分组。
  2. 通过事务分组如何找到后端集群?
    首先程序中配置了事务分组(GlobalTransactionScanner 构造方法的txServiceGroup参数),程序会通过用户配置的配置中心去寻找service.vgroup_mapping.事务分组配置项,取得配置项的值就是TC集群的名称。拿到集群名称程序通过一定的前后缀+集群名称去构造服务名,各配置中心的服务名实现不同。拿到服务名去相应的注册中心去拉取相应服务名的服务列表,获得后端真实的TC服务列表。
  3. 为什么这么设计,不直接取服务名?
    这里多了一层获取事务分组到映射集群的配置。这样设计后,事务分组可以作为资源的逻辑隔离单位,当发生故障时可以快速failover。

关于grouplist问题说明下

  1. 什么时候会用到file.conf中的default.grouplist?
    当registry.type=file时会用到,其他时候不读。
  2. default.grouplist的值列表是否可以配置多个?
    可以配置多个,配置多个意味着集群,但当store.mode=file时,会报错。原因是在file存储模式下未提供本地文件的同步,所以需要使用store.mode=db,通过db来共享TC集群间数据
  3. 是否推荐使用default.grouplist?
    不推荐,如问题1,当registry.type=file时会用到,也就是说这里用的不是真正的注册中心,不具体服务的健康检查机制当tc不可用时无法自动剔除列表,推荐使用nacos 、eureka、redis、zk、consul、etcd3、sofa。registry.type=file或config.type=file 设计的初衷是让用户再不依赖第三方注册中心或配置中心的前提下,通过直连的方式,快速验证seata服务。

5、TCC事务代码

(1)、创建订单核心代码

@GlobalTransactional
@Override
public void create(Order order) {
// 从全局唯一id发号器获得id
Long orderId = easyIdGeneratorClient.nextId("order_business");
order.setId(orderId);
String xid = RootContext.getXID();
log.info("New Transaction Begins: " + xid);
// orderMapper.create(order);

// 这里修改成调用 TCC 第一节端方法
orderTccAction.prepareCreateOrder(
null,
order.getId(),
order.getUserId(),
order.getProductId(),
order.getCount(),
order.getMoney());

// 修改库存
storageClient.decrease(order.getProductId(), order.getCount());

// 修改账户余额
accountClient.decrease(order.getUserId(), order.getMoney());

}

说明:
1、通过添加 @GlobalTransactional注解,开启seata全局分布式事务。
2、通过生成全局的订单id控制事务的幂等性。

(2)、OrderTccAction

@LocalTCC
public interface OrderTccAction {
/*
第一阶段的方法
通过注解指定第二阶段的两个方法名

BusinessActionContext 上下文对象,用来在两个阶段之间传递数据
@BusinessActionContextParameter 注解的参数数据会被存入 BusinessActionContext
*/
@TwoPhaseBusinessAction(name = "orderTccAction", commitMethod = "commit", rollbackMethod = "rollback")
boolean prepareCreateOrder(BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "orderId") Long orderId,
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "count") Integer count,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);

// 第二阶段 - 提交
boolean commit(BusinessActionContext businessActionContext);

// 第二阶段 - 回滚
boolean rollback(BusinessActionContext businessActionContext);

}

说明:
1、通过在接口上添加@LocalTCC注解,声明本地TCC事务控制接口
2、通过@TwoPhaseBusinessAction注解,声明Try、Confirm、Cancel三个阶段对应的具体方法。

(3)、OrderTccActionImpl

@Component
@Slf4j
public class OrderTccActionImpl implements OrderTccAction {
@Autowired
private OrderMapper orderMapper;

@Transactional(rollbackFor = Exception.class)
@Override
public boolean prepareCreateOrder(BusinessActionContext businessActionContext, Long orderId, Long userId, Long productId, Integer count, BigDecimal money) {
log.info("创建 order 第一阶段,预留资源 - "+businessActionContext.getXid());

//因为orderId是唯一的,不能重复执行,满足幂等性, 创建状态为0(创建中)的订单
Order order = new Order(orderId, userId, productId, count, money, 0);
orderMapper.create(order);

//模拟异常
/* if (Math.random() < 0.9999) {
throw new RuntimeException("模拟try阶段出现 异常");
}*/
//事务成功,保存一个标识,供第二阶段进行判断
ResultHolder.setResult(getClass(), businessActionContext.getXid(), "p");
return true;
}

@Transactional(rollbackFor = Exception.class)
@Override
public boolean commit(BusinessActionContext businessActionContext) {
log.info("创建 order 第二阶段提交,修改订单状态1 - "+businessActionContext.getXid());

// 防止幂等性,如果commit阶段重复执行则直接返回
if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
return true;
}

//Long orderId = (Long) businessActionContext.getActionContext("orderId");
long orderId = Long.parseLong(businessActionContext.getActionContext("orderId").toString());
//确认提交,将订单状态修改为1(创建完成)
orderMapper.updateStatus(orderId, 1);

//提交成功是删除标识
ResultHolder.removeResult(getClass(), businessActionContext.getXid());
return true;
}

@Transactional(rollbackFor = Exception.class)
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
log.info("创建 order 第二阶段回滚,删除订单 - "+businessActionContext.getXid());

//第一阶段没有完成的情况下,不必执行回滚(空回滚处理)
//因为第一阶段有本地事务,事务失败时已经进行了回滚。
//如果这里第一阶段成功,而其他全局事务参与者失败,这里会执行回滚
//幂等性控制:如果重复执行回滚则直接返回
if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
return true;
}

//创建识别,执行Cancel操作,删除临时订单
//Long orderId = (Long) businessActionContext.getActionContext("orderId");
long orderId = Long.parseLong(businessActionContext.getActionContext("orderId").toString());
orderMapper.deleteById(orderId);

//回滚结束时,删除标识
ResultHolder.removeResult(getClass(), businessActionContext.getXid());
return true;
}
}

说明:
注意TCC具体的方法实现中,对幂等、空回滚、悬挂等问题的解决。
核心逻辑解说:

  1. prepareCreateOrder方法对应try阶段,会根据唯一订单号创建临时状态订单,并在当前类下注入事务id
  2. commit对应confirm阶段,首先查看当前类是否和事务ID(businessActionContext.getXid())有关联,没有关联就直接返回true,不进行真实的提交逻辑。如果关联了事务id,则修改订单状态为已完成,并移除关联的事务ID。
  3. rollback方法对应Cancel阶段,也是首先判断当前类下是否与businessActionContext.getXid()相关联,没有关联就直接返回true防止出现空回滚。如果有关联,则执行回滚操作,根据订单id删除临时状态的订单记录。
  4. 注意TCC的3个实现方法都添加了@Transactional注解开启了事务控制,保证本地分支事务的ACID特性。

(4)、订单表说明

CREATE TABLE `order` (
`id` bigint(11) NOT NULL,
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`count` int(11) DEFAULT NULL COMMENT '数量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金额',
`status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

说明:
在TCC模式下,由于需要资源预留,一般都会在事务参与的表中添加一个资源预留字段。
在订单表中的呈现是新增了订单状态字段,记录创建中状态的订单。

(5)、order服务项目结构

Seata TCC模式实战_TCC_14


说明:

1、远程调用都采用feign调用,统一存放在feign目录下

2、tcc相关的接口,统一放在tcc目录下

3、由于order服务采用的是spring-cloud-starter-alibaba-seata 2.2.1.RELEASE版本,seata-spring-boot-starter为1.4.1版本,order中采用的是属性配置项设置的seata相关属性,省略了file.conf和registry.conf文件。

(6)、account服务核心代码

@Component
@Slf4j
public class AccountTccActionImpl implements AccountTccAction {
@Autowired
private AccountMapper accountMapper;

@Transactional(rollbackFor = Exception.class)
@Override
public boolean prepareDecreaseAccount(BusinessActionContext businessActionContext, Long userId, BigDecimal money) {
log.info("减少账户金额,第一阶段锁定金额,userId="+userId+", money="+money);

//剩余可用金额
Account account = accountMapper.selectById(userId);
if (account.getResidue().compareTo(money) < 0) {
throw new RuntimeException("账户金额不足");
}

/*
* 冻结可用金额
余额-money
冻结+money
*/
accountMapper.updateFrozen(userId, account.getResidue().subtract(money), account.getFrozen().add(money));

//模拟异常
if (Math.random() < 0.3) {
throw new RuntimeException("模拟异常");
}

//保存标识
ResultHolder.setResult(getClass(), businessActionContext.getXid(), "p");
return true;
}

/**
* Confirm 方法一定要在 Try 方法之后执行。因此,Confirm 方法只需要关注重复提交的问题。
* @param businessActionContext
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public boolean commit(BusinessActionContext businessActionContext) {
long userId = Long.parseLong(businessActionContext.getActionContext("userId").toString());
BigDecimal money = new BigDecimal(businessActionContext.getActionContext("money").toString());
log.info("减少账户金额,第二阶段,提交,userId="+userId+", money="+money);

//防止重复提交,确认try方法已经执行
if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
return true;
}

accountMapper.updateFrozenToUsed(userId, money);

//删除标识
ResultHolder.removeResult(getClass(), businessActionContext.getXid());
return true;
}

@Transactional(rollbackFor = Exception.class)
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
long userId = Long.parseLong(businessActionContext.getActionContext("userId").toString());
BigDecimal money = new BigDecimal(businessActionContext.getActionContext("money").toString());

//防止重复提交,确保try方法已经执行,防止空回滚
if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
return true;
}

log.info("减少账户金额,第二阶段,回滚,userId="+userId+", money="+money);

accountMapper.updateFrozenToResidue(userId, money);

//删除标识
ResultHolder.removeResult(getClass(), businessActionContext.getXid());
return true;
}

}

说明:
账户金额扣减TCC逻辑的3个阶段说明

  1. prepareDecreaseAccount方法对应Try阶段,主要是执行资源预留,将要扣减的金额先保留到冻结金额中。
  2. commit方法对应Confirm阶段,事务提交过程,主要负责将冻结的金额真正扣除。
  3. rollback方法对应Cancel阶段,事务回滚,主要负责将冻结的金额解冻返回到账户的余额中。

(7)、account表

CREATE TABLE `account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`total` decimal(10,0) DEFAULT NULL COMMENT '总额度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用余额',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度',
`frozen` decimal(10,0) DEFAULT '0' COMMENT 'TCC事务锁定的金额',
PRIMARY KEY (`id`),
UNIQUE KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

说明:
主要是新增了frozen字段,用来保存暂时冻结的金额。

6、测试

  1. 先启动db-init服务,初始化数据库
  2. 再启动eureka-server服务,作为注册中心
  3. 然后启动其他服务。

    发起创建订单请求:​​http://localhost:8083/create?userId=1&productId=1&count=10&mnotallow=100​​

7、异常模拟

分别在TCC的各个阶段添加模拟的异常,看程序的执行情况。

//模拟try阶段异常
if (Math.random() < 0.9999) {
throw new RuntimeException("模拟try阶段出现异常");
}

if (Math.random() < 0.9999) {
throw new RuntimeException("模拟commit阶段异常");
}

if (Math.random() < 0.9999) {
throw new RuntimeException("模拟cancel阶段异常");
}

说明:

  1. 事务的执行顺序是:创建订单——》修改库存——》减账户余额,
    创建订单的try阶段出现异常,会触发创建订单TCC操作中的Cancel操作进行空回滚;
    修改库存的try阶段出现异常,会触发创建订单TCC操作中的Cancel操作以及修改库存TCC操作中的Cancel操作进行空回滚;
    减账户余额的try阶段出现异常,会触发创建订单TCC操作中的Cancel操作以及扣减库存的TCC操作中的Cancel操作,减账户余额的TCC操作中的Cancel操作进行空回滚。
  2. Confirm操作出现异常后,会不停的重试,直到执行成功。
  3. Cancel操作出现异常后,也会不停重试,直到执行成功。
    (针对这里Confirm、Cancel的操作建议加入重试次数,失败一定操作后停止,记录相关记录,后面人工介入处理)。

8、seata tcc模式实战源码

​GitHub地址​

总结

1、理解TCC模式的底层逻辑:核心是将一个完整的事务分成了2个阶段,一阶段是所有事务参与者RM都注册到TC事务协调器,并发起分支事务请求进行资源预留,然后主动向TC汇报执行结果。二阶段TC事务协调器会根据收到的所有RM一阶段分支事务的执行结果来判断让RM继续执行提交操作还是回滚操作。
2、知道TCC模式和AT模式差别以及相互之间的优势。
3、实现TCC需要自己实现prepare、commit、rollback方法,并考虑空回滚、幂等、悬挂等问题。
4、知道怎么使用Seata框架实现 TCC事务。
5、需要在相关表中添加字段,用来保存预留资源。

很多人对分布式事务都心存畏惧,一是工作中接触的机会少,二是网上可以参考的实际可用的案例真的太少,大多数小伙伴都只是背了下各种分布式事务的相关实现方案的理论,而没有实际实现经验。

推荐大家都自己动手实战一番,做到心中有数,遇事不慌,而不是空谈理论。




标签:实战,事务,seata,spring,order,TCC,cloud,Seata
From: https://blog.51cto.com/u_15905482/5919994

相关文章

  • 还不会分布式事务,seata xa模式入门实战送上
    文章目录​​前言​​​​一、什么是seata?​​​​二、seata原理说明​​​​1、角色说明​​​​2、什么是Seata的事务模式?​​​​三、SEATA的分布式案例​​​​1、业......
  • OAuth2.0实战(三)用户信息加载
    SpringSecurity内置了三种用户存储方式:1、基于内存2、基于数据库查询语句3、自定义UserDetailsService服务来获取这里的用户存储指的是,从哪里获取用户的信息1、基于内......
  • k8s授权管理介绍与实战(RBAC)
    授权管理授权发生在认证成功之后,通过认证就可以知道请求用户是谁,然后Kubernetes会根据事先定义的授权策略来决定用户是否有权限访问,这个过程就称为授权。每个发送到ApiS......
  • 云数据库FinOps实战复盘
    历时三个多月的HBase成本优化项目按照预期交付了,HBase云数据库月度成本下降了32.5%,超出预期达成目标。我们对本次HBase成本优化项目进行深度复盘,并进一步尝试总结云数据库......
  • gin框架项目实战系列汇总
    最近打算整理重构项目的一些使用心得,打算做以下系列更新:gin-注册路由gin-中间件gin-http/https配置gin-配置初始化-vipergin-错误定义gin-统一响应responsegin-zap......
  • Python爬虫实战,requests模块,Python抓取虎牙直播美女封面图片
    前言今天给大家的介绍Python爬取海量美女图片并保存本地。开发工具Python版本:3.8相关模块:requests模块multiprocessing模块urllib模块json模块环境搭建安装Pyth......
  • vulnhub靶场渗透实战13-driftingblues3
    ​靶机下载地址:https://download.vulnhub.com/driftingblues/driftingblues3.ovavbox导入,网络模式桥接,靶机模式为简单。一:信息收集1;直接老样子吧,arp主机发现之后,nmap扫......
  • Git实战(五)| 让工作更高效,搞定Git的分支管理
    上一篇讲到Git的分支管理实操,在线合并和本地合并都进行了实操。毕竟:光说不练是假把式。而只练不整理,只能是傻把式了。分支管理到底如何进行管理呢?先以GitLab上的一张经典......
  • Python爬虫实战,requests模块,Python爬取网易云歌曲并保存本地
    前言今天给大家简单演示的爬取了一下某易云歌曲的排行榜信息,最后将音乐保存到本地开发工具Python版本:3.6.4相关模块:requests模块re模块os模块环境搭建安装Pyth......
  • 项目——基于GPS的种树机器人路径规划实战及详解
    项目——基于GPS的种树机器人路径规划实战及详解​​一、前言​​​​二、设计思路​​​​1、坐标系的转换​​​​2、输入的区域摆法及关系式​​​​3、设计流程图​​​......