32-微服务技术栈(高级):分布式事务Seata的TCC模式

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 在分布式架构系统中,服务不止一个,一个完整的业务链路肯定也不止调用一个服务,此时每个服务都有自己的数据库增删改查,而每一个写操作对应一个本地事务。如果想要确保全部的业务状态一致,也就意味着需要所有的本地事务状态一致,这在我们之前的学习中肯定是不具备的,如何做到跨服务、跨数据源的事务一致性将是本章节的重点学习内容。

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留;
  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
  • Cancel:预留资源释放,可以理解为try的反向操作。

1.流程分析

举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。

  • 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30

初始余额:

余额充足,可以冻结:

此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。

  • 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30

确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:

此时,总金额 = 冻结金额 + 可用金额 = 0 + 70  = 70元

  • 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30

需要回滚,那么就要释放冻结金额,恢复可用金额:

2.Seata的TCC模型

Seata中的TCC模型依然延续之前的事务架构,如图:

3.优缺点

TCC模式的每个阶段是做什么的?

  • Try:资源检查和预留
  • Confirm:业务执行和提交
  • Cancel:预留资源的释放

TCC的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC的缺点是什么?

  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理

4.事务悬挂和空回滚

1)空回滚

当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚

如图:

执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。

2)业务悬挂

对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂

执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂

5.实现TCC模式

解决空回滚和业务悬挂问题,必须要记录当前事务状态,是在try、还是cancel?

1)思路分析

这里我们定义一张表;在 seata_demo数据库中创建如下表:

CREATE TABLE `account_freeze_tbl`(

   `xid` varchar(128) NOT NULL,

   `user_id` varchar(255) DEFAULT NULL COMMENT'用户id',

   `freeze_money`int(11) unsigned DEFAULT'0'COMMENT'冻结金额',

   `state`int(1) DEFAULT NULL COMMENT'事务状态,0:try,1:confirm,2:cancel',

   PRIMARY KEY(`xid`)USING BTREE

)ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

其中:

  • xid:是全局事务id
  • freeze_money:用来记录用户冻结金额
  • state:用来记录事务状态

那此时,我们的业务该怎么做呢?

  • Try业务:
  • 记录冻结金额和事务状态到account_freeze表
  • 扣减account表可用金额
  • Confirm业务
  • 根据xid删除account_freeze表的冻结记录
  • Cancel业务
  • 修改account_freeze表,冻结金额为0,state为2
  • 修改account表,恢复可用金额
  • 如何判断是否空回滚?
  • cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
  • 如何避免业务悬挂?
  • try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务

接下来,我们改造account-service,利用TCC实现余额扣减功能。

2)声明TCC接口

TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明,

我们在account-service项目中的cn.itcast.account.service包中新建一个接口,声明TCC三个接口:

package cn.itcast.account.service;


import io.seata.rm.tcc.api.BusinessActionContext;

import io.seata.rm.tcc.api.BusinessActionContextParameter;

import io.seata.rm.tcc.api.LocalTCC;

import io.seata.rm.tcc.api.TwoPhaseBusinessAction;


@LocalTCC

public interface AccountTCCService {


   /**

    * 根据用户id扣减余额,记录冻结金额

    * commitMethod 提交时对于的方法;rollbackMethod回滚时对于的方法

    * @param userId 用户id

    * @param money 扣减金额

    */

   @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")

   void deduct(@BusinessActionContextParameter("userId")String userId,

               @BusinessActionContextParameter("money")int money);


   /**

    * 执行TCC事务时,提交事务时执行的方法

    * @param ctx 上下文对象

    * @return 执行结果

    */

   Boolean confirm(BusinessActionContext ctx);

   /**

    * 执行TCC事务时,回滚事务时执行的方法

    * @param ctx 上下文对象

    * @return 执行结果

    */

   Boolean cancel(BusinessActionContext ctx);

}

3)编写实现类

account-service服务中的cn.itcast.account.service.impl包下新建一个类,实现TCC业务:

package cn.itcast.account.service.impl;


import cn.itcast.account.entity.AccountFreeze;

import cn.itcast.account.mapper.AccountFreezeMapper;

import cn.itcast.account.mapper.AccountMapper;

import cn.itcast.account.service.AccountTCCService;

import io.seata.core.context.RootContext;

import io.seata.rm.tcc.api.BusinessActionContext;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;


@Service

public class AccountTCCServiceImpl implements AccountTCCService {


   @Autowired

   private AccountMapper accountMapper;


   @Autowired

   private AccountFreezeMapper accountFreezeMapper;


   @Override

   @Transactional

   public void deduct(String userId, int money) {

       //获取事务id

       String xid = RootContext.getXID();

       //处理业务悬挂

       AccountFreeze freeze = accountFreezeMapper.selectById(xid);

       if (freeze != null) {

           //已经cancel过,拒绝业务

           return;

       }


       //1 扣减余额

       accountMapper.deduct(userId, money);


       //2 记录冻结金额

       AccountFreeze accountFreeze = new AccountFreeze();

       accountFreeze.setXid(xid);

       accountFreeze.setUserId(userId);

       accountFreeze.setFreezeMoney(money);

       accountFreeze.setState(AccountFreeze.State.TRY);

       accountFreezeMapper.insert(accountFreeze);

   }


   @Override

   public Boolean confirm(BusinessActionContext ctx) {

       //删除冻结金额

       String xid = ctx.getXid();

       int count = accountFreezeMapper.deleteById(xid);

       return count==1;

   }


   @Override

   public Boolean cancel(BusinessActionContext ctx) {

       //获取全局事务id

       String xid = ctx.getXid();

       //用户id

       String userId = ctx.getActionContext("userId").toString();

       AccountFreeze accountFreeze = accountFreezeMapper.selectById(xid);


       //处理空回滚

       if (accountFreeze == null) {

           accountFreeze = new AccountFreeze();

           accountFreeze.setXid(xid);

           accountFreeze.setUserId(userId);

           accountFreeze.setFreezeMoney(0);

           accountFreeze.setState(AccountFreeze.State.CANCEL);

           accountFreezeMapper.insert(accountFreeze);

           return true;

       }


       //幂等处理

       if (accountFreeze.getState() == AccountFreeze.State.CANCEL) {

           //说明已经回滚过了;不需要再处理

           return true;

       }


       //1 回退金额

       accountMapper.refund(userId, accountFreeze.getFreezeMoney());


       //2 更新冻结金额为0和状态

       accountFreeze.setFreezeMoney(0);

       accountFreeze.setState(AccountFreeze.State.CANCEL);

       int count = accountFreezeMapper.updateById(accountFreeze);


       return count==1;

   }

}

改造 src/main/java/cn/itcast/account/web/AccountController.java 如下:

相关文章
|
18天前
|
自然语言处理 监控 Dubbo
Seata常见问题之使用tcc模式配置yml如何解决
Seata 是一个开源的分布式事务解决方案,旨在提供高效且简单的事务协调机制,以解决微服务架构下跨服务调用(分布式场景)的一致性问题。以下是Seata常见问题的一个合集
|
18天前
|
数据库
|
18天前
|
存储 分布式数据库
GaussDB分布式与单机模式的比较
【4月更文挑战第7天】GaussDB分布式与单机模式的比较
1797 5
|
18天前
|
存储 监控 安全
金石推荐 | 【分布式技术专题】「单点登录技术架构」一文带领你好好认识以下Saml协议的运作机制和流程模式
金石推荐 | 【分布式技术专题】「单点登录技术架构」一文带领你好好认识以下Saml协议的运作机制和流程模式
83 1
|
18天前
|
NoSQL Java 数据库
Seata常见问题之xa模式下插入一条数据再更新这条数据会报错如何解决
Seata 是一个开源的分布式事务解决方案,旨在提供高效且简单的事务协调机制,以解决微服务架构下跨服务调用(分布式场景)的一致性问题。以下是Seata常见问题的一个合集
112 2
|
18天前
|
Java 关系型数据库 微服务
Seata常见问题之项目一直启动不成功如何解决
Seata 是一个开源的分布式事务解决方案,旨在提供高效且简单的事务协调机制,以解决微服务架构下跨服务调用(分布式场景)的一致性问题。以下是Seata常见问题的一个合集
119 0
|
18天前
|
存储 Java Nacos
Seata常见问题之xa模式出现错误xid is not valid如何解决
Seata 是一个开源的分布式事务解决方案,旨在提供高效且简单的事务协调机制,以解决微服务架构下跨服务调用(分布式场景)的一致性问题。以下是Seata常见问题的一个合集
|
18天前
|
数据库 开发者
Seata的 TCC 模式
Seata的 TCC 模式
|
18天前
|
NoSQL Java 关系型数据库
【Redis系列笔记】分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
309 2