分布式事务解决方案--TCC

什么是TCC

TCC是try、confirm、cancel三个单词的缩写。

  • try:预留和锁定业务需要的资源。
  • conirm:业务确认,当所有的分支事务都成功时执行。
  • cancel:撤销。如果有一个分支事务执行失败,执行。把预留阶段的资源撤销,相当于事务回滚

执行流程图如下:
在这里插入图片描述
执行流程说明:

  1. 全局事务发起者向申请开启一个全局事务,并生成一个代表该全局事务的全局事务ID。该全局事务ID会在整个分布式调用链中传递,用于记录事务上下文,追踪和记录状态。
  2. 全局事务发起者调用其他服务的try方法,预留资源
  3. 各个事务参与者将try的执行结果返回给全局事务发起者
  4. 全局事务发起者根据try的执行结果决定提交或回滚
  5. 如果是提交的话,事务管理者调用所有分支事务的confirm方法完成事务提交;反之,事务管理者调用所有分支事务的cancel方法完成事务回滚。

第一次接触的可能对预留资源不太理解,比如我要转给你100块,那我就得先从我的账户中扣除100块,这100块就是我们预留的资源,如果转账失败的话,cancel就会将100块(预留资源)返回给我们。说白了,cancel就是执行try的反向操作。

在SQL上的表现就是:

try: update bank set balance = balance + 100 where userId = 1

cancel:update bank set balance = balance - 100 where userId = 1

举个例子:

需求:账户A转账30元给账户B,假设A和B账户在不同的微服务上。

账户A需求分析:

  • try:需要确保有30元可以转,所以在try中要预留30元
  • confirm:30元在try中扣除了,所以confirm中什么也不需要干
  • cancle:如果业务执行失败,cancel增加30元,实现事务回滚(撤销预留资源)

账户B需求分析:

  • try:账户B不需要
  • confirm:整个事务执行成功,账户B需要增加30元,所以confirm增加30元
  • cancel:由于try啥也没干,所以cancel也啥也不用干

伪代码如下:
A:

try:
	检查余额是否够30
	扣减30元
confirm:
	空
cancel:
	增加30元

B:

try:
	空
confirm: 
	增加30元 
cancel:
	

正常流程:A.try => B.try => A.confirm => B.confirm

异常流程:A.try => B.try => A.cancel=> B.cancel (A或B其中一个在try阶段出现异常)

至此我们已经了解TCC的执行流程, 但是TCC还存在一些其他需要的问题。

TCC的三个注意问题

TCC认为cancel和confirm是一定要执行成功的,所以如果这俩个地方出现异常,它会有重试机制

空回滚

在没有调用try的情况下,调用了cancel,造成空回滚。

出现该原因的情况:当一个分支事务所在的服务宕机或异常,分支事务调用记录为异常,这个时候其实是没有执行try阶段的,当故障恢复后,分布式事务进行回滚会调用第二阶段的cancel方法,从而形成回滚。

解决思路:如果我们能知道第一阶段是否执行,我们就能判断出当前是否为空回滚,我们可以额外增加一张分支事务记录表,其中有全局事务ID和分支事务ID,在第一阶段try方法中给表插入一条记录,表示记录存在了,在cancel阶段的时候去查数据库,如果记录存在,则正常回滚,记录不存在则为空回滚。

幂等

由于TCC存在重试机制,如果我们没有做幂等性的话,会导致confirm和cancel方法执行多次,造成严重的数据不一致。同样的我们的try方法也应该做幂等性,因为try方法也可能被多次调用。

同样的我们可以添加一张事务记录表,在confirm阶段和cancel阶段分别往自己的事务记录表中插入进入,在执行前先去查记录是否存在,存在说明之前执行过,不在执行。

悬挂

悬挂指的是cancel阶段在try阶段之前执行,导致锁定的资源永远无法被释放。

出现该原因的情况,RPC在调用分支事务try方法时,出现网络拥堵,导致超时,超时后TM(事务管理者)就会通知RM执行cancel方法进行事务回滚,可能等完成回滚后,RPC请求才到达分支事务,执行try方法,这时候预留的资源就永远也无法被释放。

解决思路:在第二阶段执行后,就不应该执行第一阶段。在执行第一阶段前,先判断在"分支事务记录"表中是否存在第二阶段的事务记录,如果有则不执行try。

https://www.cnblogs.com/jajian/p/10014145.html作实现回滚。

所以说,之前转账的例子伪代码应该改为:
A:

try:
	幂等检查处理
	悬挂检查处理
	检查余额是否够30
	扣减30元
confirm:
	空
cancel:
	幂等检查处理
	空回滚检查处理
	增加30元

B:

try:
	空
confirm: 
	幂等检查处理
	增加30元 
cancel:
	空

使用Himily实现TCC事务

业务说明

模拟俩个账户转账交易过程,俩个账户分别在不同的银行(张三在bank1、李四在bank2),bank1、bank2是俩个微服务。交易过程是,张三给 李四转账指定金额。

上述交易步骤,要么一起成功,要么一起失败,必须是一个整体性的事务。

程序组成部分

数据库:MySQL-5.7.25
JDK:64位 jdk1.8.0_201
微服务:spring-boot-2.1.3、spring-cloud-Greenwich.RELEASE
Hmily:hmily-springcloud.2.0.4-RELEASE

微服务及数据库的关系 :

dtx/dtx-tcc-demo/dtx-tcc-demo-bank1 银行1,操作张三账户, 连接数据库bank1

dtx/dtx-tcc-demo/dtx-tcc-demo-bank2 银行2,操作李四账户,连接数据库bank2

服务注册中心:dtx/discover-server

环境准备

1、引入Himily maven依赖

<dependency> 
	<groupId>org.dromara</groupId>
	<artifactId>hmily‐springcloud</artifactId>
	<version>2.0.4‐RELEASE</version> 
</dependency>

2、配置Himily

2.1 编写配置文件

org:
  dromara:
    hmily :
      serializer : kryo
      recoverDelayTime : 30
      retryMax : 30
      scheduledDelay : 30
      scheduledThreadMax :  10
      repositorySupport : db
      started: true
      hmilyDbConfig :
        driverClassName  : com.mysql.jdbc.Driver
        url :  jdbc:mysql://localhost:3306/hmily?useUnicode=true
        username : root
        password : root

2.2 编写配置类

@Bean
    public HmilyTransactionBootstrap hmilyTransactionBootstrap(HmilyInitService hmilyInitService){
        HmilyTransactionBootstrap hmilyTransactionBootstrap = new HmilyTransactionBootstrap(hmilyInitService);
        hmilyTransactionBootstrap.setSerializer(env.getProperty("org.dromara.hmily.serializer"));
        hmilyTransactionBootstrap.setRecoverDelayTime(Integer.parseInt(env.getProperty("org.dromara.hmily.recoverDelayTime")));
        hmilyTransactionBootstrap.setRetryMax(Integer.parseInt(env.getProperty("org.dromara.hmily.retryMax")));
        hmilyTransactionBootstrap.setScheduledDelay(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledDelay")));
        hmilyTransactionBootstrap.setScheduledThreadMax(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledThreadMax")));
        hmilyTransactionBootstrap.setRepositorySupport(env.getProperty("org.dromara.hmily.repositorySupport"));
        hmilyTransactionBootstrap.setStarted(Boolean.parseBoolean(env.getProperty("org.dromara.hmily.started")));
        HmilyDbConfig hmilyDbConfig = new HmilyDbConfig();
        hmilyDbConfig.setDriverClassName(env.getProperty("org.dromara.hmily.hmilyDbConfig.driverClassName"));
        hmilyDbConfig.setUrl(env.getProperty("org.dromara.hmily.hmilyDbConfig.url"));
        hmilyDbConfig.setUsername(env.getProperty("org.dromara.hmily.hmilyDbConfig.username"));
        hmilyDbConfig.setPassword(env.getProperty("org.dromara.hmily.hmilyDbConfig.password"));
        hmilyTransactionBootstrap.setHmilyDbConfig(hmilyDbConfig);
        return hmilyTransactionBootstrap;
    }

3、在启动类上添加@EnableAspectJAutoProxy并将org.dromara.hmily加入到扫描包中

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients(basePackages = {"cn.itcast.dtx.tccdemo.bank1.service.feign"})
@ComponentScan({"cn.itcast.dtx.tccdemo.bank1","org.dromara.hmily"})
public class Bank1TccServer {

	public static void main(String[] args) {
		SpringApplication.run(Bank1TccServer.class, args);
	}

}

代码开工

dtx-tcc-demo-bank1

bank1是转账的角色,按照我们之前的分析,他应该是这样的。

try:
	幂等检查处理
	悬挂检查处理
	检查余额是否足够
	扣减金额
confirm:
	空
cancel:
	幂等检查处理
	空回滚检查处理
	增减扣减的金额

代码比较多,我就不全贴出来,只点几个比较重要的点:
service层:

	//表明该方法是个try方法,并指定cancel和confirm方法
	@Hmily(cancelMethod = "cancel",confirmMethod = "confirm")
    @Transactional
    public void updateAccountBalance(String accountNo, Double amount) {
    	//1.幂等处理
    	//2.悬挂处理
    	//3.执行业务代码(检查余额,然后扣钱)
	    //4.分支事务记录表中保存try的执行记录
    }

@Transactional
    public void cancel(String accountNo, Double amount) {
        //1. 幂等性验证
        //2. 空回滚
		//3. 撤销预留资源
		//4. 分支事务表中保存cancel执行记录
    }

    public void confirm(String accountNo, Double amount) {
        //什么也不需要干
    }

dtx-tcc-demo-bank2

bank2是收钱的角色,按照我们之前的分析,他应该这样:

try:
	空
confirm: 
	幂等检查处理
	增加金额
cancel:
	空

service层:

@Hmily(confirmMethod = "confirmMethod",cancelMethod = "cancelMethod")
 public void updateAccountBalance(String accountNo, Double amount) {
     //啥也不做
 }

 @Transactional
 public void confirmMethod(String accountNo, Double amount) {
     //1.幂等验证
     //2.执行业务代码
 	 //3.添加confirm日志,用于幂等性判断
 }

 public void cancelMethod(String accountNo, Double amount) {
     //啥也不干
 }

如果你有兴趣去我的码云看TCC实现代码,你就会发现,我们真正的业务代码只有几行,但是TCC的代码就有十几行,由此可以看出TCC对于业务代码有较大的入侵,而且代码工作量巨大!

码云地址

从代码中我们可以看出来,由于TCC要求每个分支事务都必须提供try、confirm、cancel三个方法,所以TCC对于业务代码有较大的入侵,而且代码工作量巨大!

当然,也正是因为他是在代码层面上实现的分布式事务,所以TCC的使用范围也会更大,可以实现跨数据库、跨不同的业务系统的分布式事务。

项目测试请求http://localhost:56081/bank1/transfer?amount=1

测试用例:

  • 场景1:amount=1,bank1和bank2的try方法都执行成功,且confirm方法也执行成功。
  • 场景2:amount=2,bank1的try方法在远程调用bank2之前执行失败。验证bank2是否会出现空回滚
  • 场景3:amount=3,bank1的try远程调用bank2成功后,后续业务执行失败。验证bank2是否成功回滚
  • 场景4:amount=4,bank1和bank2的try方法都执行成功,但是bank2的comfirm方法执行失败。验证confirm是否会重试(存在)
  • 场景5:amount=5,bank1和bank2的try方法有一个失败,且bank1的cancel也失败。验证cancel是否存在重试机制。(存在)

TCC认为confirm和cancel方法是一定会成功的,如果出现异常,那么就是我们自己写的代码有问题!

在这里插入图片描述
脑图链接地址

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值