首页 > 其他分享 >使用分布式事务 Seata 的 TCC 模式

使用分布式事务 Seata 的 TCC 模式

时间:2023-12-05 22:57:38浏览次数:32  
标签:事务 Seata freeze cancel import com TCC 分布式

Seata 的 TCC 模式需要通过人工编码来实现数据的回滚恢复,有点麻烦,但是性能最高。TCC 是 3 个方法的首字母缩写,即 Try 方法、Confirm 方法、Cancel 方法。Try 方法进行资源的检查和冻结,Confirm 方法是当所有事务都成功后调用的方法,Cancel 方法是当整体事务中某个分支事务失败时调用的数据回滚恢复方法,相当于是 Try 方法的反向操作。

在一个项目中的 Seata 事务中,AT 模式和 TCC 模式可以并存。TCC 模式是有使用场景的,对于金额扣除和库存扣除,能够实现金额冻结和库存冻结,因此可以使用 TCC 模式。对于下单操作来说,只能进行添加或删除回滚操作,没有冻结的场景,因此只能使用 AT 模式,无法使用 TCC 模式。本篇博客仍然使用上篇博客的 Demo 进行改造,仅对金额的扣除实现 TCC 模式进行演示。


一、搭建工程

复制一份上篇博客的 Demo,为了区分,我将工程的名字改为 springcloud_seata_tcc,如下所示:

image

在 application.yml 中,由于 data-source-proxy-mode 只能配置两种值:XA 和 AT,没有 TCC 这种值,因此还是配置为 AT,对于 OrderService 和 StockService 仍然使用 AT 模式,对于 AccountService 虽然配置为 AT,但是代码中我们会使用 @LocalTCC 注解编写一个新的 Service 类,表示使用 TCC 模式。

image


二、代码实现

由于需要冻结金额,因此需要在我们自己的业务数据库 seatatest 中创建一张记录冻结金额和事务状态的表

CREATE TABLE `tb_account_freeze`  (
  `xid` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '事务id',
  `user_id` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户id',
  `freeze_money` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '冻结金额',
  `state` int(1) NULL DEFAULT NULL COMMENT '事务状态,1:try,0:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

为了对该表进行增删改查,因此就需要创建对应的实体类和 mapper 文件:

package com.jobs.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
@TableName("tb_account_freeze")
public class AccountFreeze {

    @TableId(type = IdType.INPUT)
    private String xid;

    private String userId;

    private Integer freezeMoney;

    //1-try状态,0-cancel状态
    private Integer state;
}
package com.jobs.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jobs.pojo.AccountFreeze;
import org.apache.ibatis.annotations.Mapper;

//由于使用 mybatis plus 框架,因此这里就只需要集成 BaseMapper传入实体类即可生成相应的增删改查方法
@Mapper
public interface AccountFreezeMapper extends BaseMapper<AccountFreeze> {
}

最后我们新建一个全新的 Service 类:AccountTccService,专门用来实现 Seata 的 TCC 模式:

package com.jobs.service;

import com.jobs.mapper.AccountFreezeMapper;
import com.jobs.mapper.AccountMapper;
import com.jobs.pojo.AccountFreeze;
import io.seata.core.context.RootContext;
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;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

//TCC 是事务是 3 中操作的首字母缩写,即 try(执行操作),confirm(确认提交),cancel(数据回滚)
@LocalTCC
@Slf4j
@Service
public class AccountTccService {

    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private AccountFreezeMapper freezeMapper;

    //该注解配置了 tcc 事务的 3 个方法:
    //name 配置 try 方法
    //commitMethod 配置 confirm 方法
    //rollbackMethod 配置 cancel 方法
    @TwoPhaseBusinessAction(name = "minusMoney",
            commitMethod = "confirm", rollbackMethod = "cancel")
    public void minusMoney(
            //使用该注解指定的参数,
            //参数值可以在 confirm 方法和 cancel 方法的 BusinessActionContext 参数中获取到
            @BusinessActionContextParameter(paramName = "uid") String uid,
            @BusinessActionContextParameter(paramName = "money") int money) {
        //获取事务id
        String xid = RootContext.getXID();

        //为了防止业务悬挂,需要判断是否有冻结记录,如果有的话,就不能再执行 try 操作了
        AccountFreeze oldfreeze = freezeMapper.selectById(xid);
        if (oldfreeze != null) {
            return;
        }

        //减钱
        accountMapper.minusMoney(uid, money);
        //记录冻结的金额和事务状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(uid);
        freeze.setFreezeMoney(money);
        // 1 表示 try 状态,0 表示 cancel 状态
        freeze.setState(1);
        freeze.setXid(xid);
        freezeMapper.insert(freeze);
    }

    //事务成功提交的方法,此时需要删除冻结记录即可
    public boolean confirm(BusinessActionContext bac) {
        //获取事务id
        String xid = bac.getXid();
        //根据id删除冻结记录
        int count = freezeMapper.deleteById(xid);
        return true;
    }

    //数据回滚方法,此时需要恢复金额,更改冻结记录的状态
    public boolean cancel(BusinessActionContext bac) {
        //通过事务id查询冻结记录中的金额
        String xid = bac.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);

        //如果 freeze 为 null,表示之前没有执行过 try,
        //此时需要空回滚,向 tb_account_freeze 表示添加一条 cancel 状态的记录
        if (freeze == null) {
            freeze = new AccountFreeze();

            //由于在 try 方法(也就是 minusMoney 方法)的参数 uid,
            //使用了 @BusinessActionContextParameter 注解,
            //因此这里使用 BusinessActionContext.getActionContext("uid")
            //就能够获取到 uid 传入的参数值,也就是用户id的值
            String uid = bac.getActionContext("uid").toString();
            freeze.setUserId(uid);
            freeze.setFreezeMoney(0);
            // 1 表示 try 状态,0 表示 cancel 状态
            freeze.setState(0);
            freeze.setXid(xid);
            freezeMapper.insert(freeze);
            return true;
        }

        //为了防止 cancel 方法被调用了多次,这里需要幂等性判断
        //如果获取到的冻结记录,状态本身已经是 cancel 状态,则不再进行处理
        if (freeze.getState() == 0) {
            return true;
        }

        //恢复余额
        accountMapper.addMoney(freeze.getUserId(), freeze.getFreezeMoney());
        //将冻结金额清零,状态改为 cancel
        //1 表示 try 状态,0 表示 cancel 状态
        freeze.setFreezeMoney(0);
        freeze.setState(0);
        freezeMapper.updateById(freeze);
        return true;
    }
}

最后在 AccountController 类中,使用 AccountTccService 方法来进行扣钱即可:

package com.jobs.controller;

import com.jobs.service.AccountService;
import com.jobs.service.AccountTccService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/account")
@RestController
public class AccountController {

    @Autowired
    private AccountTccService accountTccService;

    @GetMapping("/minus/{uid}/{money}")
    public ResponseEntity<String> minusMoney(@PathVariable("uid") String uid,
                                             @PathVariable("money") Integer money) {
        accountTccService.minusMoney(uid, money);
        return ResponseEntity.ok("减钱成功");
    }
}

三、验证效果

当我们调用下单接口,传入的金额和库存量都满足的情况下,能够正常下单,这种情况就不演示了。

我们使用 Postman 调用下单接口,传入的金额能够满足,库存量大一些,不能满足要求,此时就会下单失败,数据回滚:

image

此时看一下 Account 服务的日志,可以看到 TCC 模式的回滚日志:

image

在我们自己的业务数据库中,记录冻结金额和事务状态的表 tb_account_freeze 表中多了一条记录:

image

由于数据进行了回滚恢复,所以该记录中金额修改为 0,状态修改为 0 (cancel 状态)


四、TCC 模式的存在问题和优缺点

TCC 模式并非所有场景都适用,如本篇博客的 Demo 中,下单就不适合适用 TCC 模式,只有能够实现资源冻结的情况,才可以使用 TCC 模式,比如本篇博客中的金额和库存量的增减场景,就可以使用 TCC 模式。

另外需要注意的是:

  • 在使用 TCC 模式实现 Try 方法时,需要考虑业务悬挂的情况。所谓业务悬挂是指由于网络原因,本分支的事务 Try 方法还没来得及执行,其它分支事务失败了,然后导致本分支事务进行了提前进行了 cancel 回滚操作,此时 Try 方法由于网络恢复执行了,导致资源冻结,但是本分支事务早已结束,后续永远不会再进行 Confirm 或 Cancel 方法的执行,此时冻结的资源就永远无法释放了。
  • 在使用 TCC 模式实现 Cancel 方法是,需要考虑空回滚的情况。所谓空回滚跟上面的业务悬挂场景相同,就是由于网络原因,本分支的事务 Try 方法还没来得及执行,其它分支事务失败了,然后导致本分支事务进行了提前进行了 cancel 回滚操作。此时的回滚操作不能进行金额的恢复操作,需要进行空回滚。

以上两种情况,本篇博客的 Demo 中在 AccountTccService 方法中都有考虑和实现。

TCC 模式的优点是:

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

TCC 模式的缺点是:

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

OK,以上就是有关 Seata 的 TCC 模式的介绍,可以下载源代码进行运行验证结果。

本篇博客的源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springcloud_seata_tcc.zip

标签:事务,Seata,freeze,cancel,import,com,TCC,分布式
From: https://www.cnblogs.com/studyjobs/p/17878504.html

相关文章

  • Seata 分布式事务
    Seata分布式事务​#Seata中间件#​Seata是2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。官网地址:http://seata.io/CAP定理和Base理论这两个在前面弄Nacos的时候已经说过......
  • StackGres 数据库平台工程,使用 Citus + Patroni 创建生产级高可用分布式 PostgreSQL
    系列StackGres,可私有部署的云原生数据库平台工程StackGres 数据库平台工程功能介绍与快速上手StackGres1.6数据库平台工程集群配置管理(K8SPods/PostgreSQL/PgBouncer)StackGres1.6数据库平台工程,集群高可用(Patroni3管理)什么是ShardedCluster(分片集群)Sha......
  • HydroOJ 踩坑指南(1)狡猾的分布式官方文档
    本系列旨在记录使用HydroOJ时的一些坑点,更全的说明文档请查看官方文档。欢迎联系本人QQ补充:2422609586.HydroOJ官方QQ群:1085853538.入门第一坑:官方文档不止一处!都说学习项目要先认真读文档,HydroOJ的文档使用了分布式阅读系统,并异地多中心部署(bushi),所以需要汇总一下:......
  • 使用Slurm集群进行分布式图计算:对Github网络影响力的系统分析
    本文分享自华为云社区《基于Slurm集群的分布式图计算应用实践:Github协作网络影响力分析》,作者:yd_263841138。1.引言Slurm(SimpleLinuxUtilityforResourceManagement)是一个针对小型Linux集群的开源、容错、高可扩展的集群管理及作业调度系统,它具备统一管理集群内的计算资源......
  • 使用分布式事务 Seata 的 AT 模式
    有了上篇博客实现XA模式的基础,本篇博客在实现AT模式时,不需要修改任何代码,只需要增加一张数据库表,修改以下application.yml配置即可实现。AT模式也是分两个阶段提交的事务模型,它缺弥补了XA模型中资源锁定周期过长的问题。其实现的两个阶段的工作原理如下:第一阶段注册分......
  • 分布式协同(万字长文)
    分布式协同分布式协同,也叫分布式协调,是在计算机网络中,不同的硬件或软件组件完成各自的任务,然后通过协同工作来解决问题。在分布式系统中,不同的节点需要进行信息的交换,以达到一致的状态。这个过程就需要分布式协调。例如,我们要保证在分布式系统中的所有节点上的数据是最新的,就需......
  • 分布式事务~seata的使用
    springcloud-nacos-seata分布式事务组件seata的使用demo,AT模式,集成nacos、springboot、springcloud、mybatis-plus,数据库采用mysqldemo中使用的相关版本号,具体请看代码。如果搭建个人demo不成功,验证是否是由版本导致,由于目前这几个项目更新比较频繁,版本稍有变化便会出现许多奇怪......
  • LAXCUS分布式操作系统的产品规划和发展阶段​
    又是一位网友的提问,把回答贴出来给大家看看,欢迎大家在下方留言交流。问:关注你们很久了,我想问一问,Laxcus分布式操作系统有没有具体的产品规划,如果有是什么,打算怎么做?你们的主要着眼点在哪里?目前Laxcus分布式操作系统发展到什么阶段了?答:感谢关注!Laxcus做为全球第一款纯粹的分布式操......
  • 对Laxcus分布式操作系统的认知、价值、痛点解决的回答​
    下面是一位网友的提问,回答贴出来供大家参考,欢迎在下方留言评论。问:Laxcus分布式操作系统有哪些与众不同的地方?它的价值在哪里?解决了哪些市场痛点?我公司现在已经使用Linux操作系统部署了一堆服务器,你如何说服我弃用Linux使用Laxcus?回答:Laxcus分布式操作系统是操作系统的一个异类,它是......
  • 分布式数据库Apache Doris HA集群部署
    ......