Springboot整合Seata实现分布式事务

原文:https://www.cnblogs.com/shuiyao3/p/17290762.html

前言

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

Seata 配置非常灵活,支持多种注册中心、配置来源(配置中心)和持久化方式。本文选择 eureka 作注册中心,本地文件作配置,用 MySQL 作持久化。

名词解释

TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器 / 事务参与者
    定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器 / 事务参与者
    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
  • TM 和 RM 都可以看成我们的业务服务,而 TM 是分布式事务开始的地方,调用链如 TM — RM — RM ~。

TC 配置

  • 在 TC GitHub Releases 下载编译好的包解压。
  • 修改 /conf/registry.conf 配置注册中心和配置来源
registry {
  type = "eureka"
  
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "seata-server"
    weight = "1"
  }
}

config {
  type = "file"
  
  file {
    name = "file.conf"
  }
}

因为使用配置类型为 file, 修改对应 /conf/file.conf,指定 db 作为存储,并配置好数据库地址
也可以直接注册中心配置中心都用nacos

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"
 
  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
    url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
    user = "root"
    password = "root"
  }
}
  • 注册(file 、nacos 、eureka、redis、zk、consul、etcd3、sofa)、配置(nacos 、apollo、zk、consul、etcd3)、存储(file、db、redis)方式有很多种方式,解压包里 conf/ 下配置文件各种配置方式都列出来了,只需要指定使用的方式,再修改对应类型的配置就行了。

  • 创建数据库执行 TC MySQL 脚本

  • 执行 bin/ 下脚本 seata-server.sh/bat 启动 TC 服务。

TM / RM 配置

  • pom.xml 添加依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <version>${spring-cloud-alibaba-seata.version}</version>
</dependency>
  • resources/ 增加 registry.conf 配置文件。同 TC 一样指明使用 eureka 作注册中心,本地 file.conf 作配置来源
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "eureka"
  
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "seata-server"
    weight = "1"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
  type = "file"

  file {
    name = "file.conf"
  }
}
  • resources/ 增加 file.conf 配置文件,修改下面项为 TC 注册到 eureka 的服务名
service {
  #transaction service group mapping
  vgroupMapping.my_test_tx_group = "seata-server"
}

AT (Automatic Transaction) 模式

  • AT 是 2PC 方案的一种实现。TM 先执行本地事务,并在 undo_log 表记录回滚日志但不提交,向 TC 申请全局锁成功后一起提交。

  • 紧接着调用后面的 RM,RM 同样记录回滚日志直接提交,不需要申请全局锁。

  • 最后所有参与者事务都执行成功,则 TC 通知提交,参与者会删掉回滚日志;如果有参与者执行失败通知 TC 后,TC 会通知参与者回滚,参与者根据日志回滚数据。

  • 全局锁是独占锁,占用期间并行的请求会在申请锁那一步等待。

  • 跟 2PC 比,AT 先提交了本地事务,不会长时间占用资源。
    在这里插入图片描述

  • 使用时只需要在 TM 服务事务方法上加注解 @GlobalTransactional,调用链后面的 RM 无需处理。

@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
    storageFeignClient.deduct(commodityCode, orderCount);
    orderFeignClient.create(userId, commodityCode, orderCount);
    if (!validData()) {
        throw new RuntimeException("账户或库存不足,执行回滚");
    }
}

今年 1 月份,Seata 开源了 AT 模式。AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

image.png

AT 模式如何做到对业务的无侵入 :

  • 一阶段:
    在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

图片3.png

  • 二阶段提交:
    二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

图片4.png

  • 二阶段回滚:
    二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

图片5.png

AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

TCC (Try Confirm Cancel) 模式

2019 年 3 月份,Seata 开源了 TCC 模式,该模式由蚂蚁金服贡献。TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。

图片6.png

TCC 三个方法描述:

  • Try:资源的检测和预留;
  • Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
  • Cancel:预留资源释放;

Seata TCC 三部操作对应 prepare、commit、rollback。

在这里插入图片描述

  • TM / RM 业务类需要一个接口,接口加上注解 @LocalTCC;业务接口上加 @TwoPhaseBusinessAction 并指明三个阶段执行的方法。然后实现类实现接口方法的具体逻辑。
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 ITccService {

    /**
     * Prepare
     */
    @TwoPhaseBusinessAction(name = "tccTest", commitMethod = "tccTestCommit", rollbackMethod = "tccTestRollback")
    String tccTest(@BusinessActionContextParameter(paramName = "param1") String param1,
                   @BusinessActionContextParameter(paramName = "param2") boolean param2);

    /**
     * Commit
     */
    boolean tccTestCommit(BusinessActionContext businessActionContext);

    /**
     * Rollback
     */
    boolean tccTestRollback(BusinessActionContext businessActionContext);
}
  • TM 则需要另外在调用事务方法的地方加注解 @GlobalTransactional。需要注意的是这个不能加到 Service 里面 。
@GlobalTransactional
@RequestMapping("/tcc/rollback")
public String purchaseRollback() {
    iTccService.tccTest(ThreadLocalRandom.current().nextInt() + "", true);
    return "全局事务提交";
}

TCC现实案例代码实现——扣除库存:
参考:https://www.cnblogs.com/studyjobs/p/17878504.html

下面代码中的AccountFreeze实体类对应表结结构:

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;
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;
    }
}

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 的失败情况,做好幂等处理

如何解决 TCC 空回滚 幂等 悬挂问题?

增加事务控制表来解决,表主要包含全局事务id(transaction_id)、分支事务id(branch_id)、状态(state:1-已初始化、2-已提交、3-已回滚)。

  • 空回滚
    try 未执行的情况下执行了 cancel,比如开启全局事务后执行 try 超时了,但事务已经开启,需要推进到终态,因此 TC 通知执行 cancel,从而形成空回滚。

解决方案:执行 try 时在事务控制表插入一条记录,标记为1-已初始化。cancel 时检查记录存在,正常回滚;如果不存在,执行空回滚。

  • 悬挂
    cancel 优先 try 执行,同样是 try 超时,然后执行了空回滚后 try 开始执行。这种情况下应该让 try 不执行。

解决方案 :cancel、commit 的时候判断如果事务控制表没记录则插入对应状态的记录,try 执行前如果发现有记录则不执行。

  • 幂等
    TC 在通知参与者提交/回滚的时候,如果没收到反馈会重复通知。

解决方式:参与者处理完后修改事务控制表中的状态,处理前判断状态如果处理过,则不做任何处理。

参考链接
Seata 官网
https://seata.io/zh-cn/
Seata 中文文档
https://seata.io/zh-cn/docs/overview/what-is-seata.html
GitHub - TC 服务 Releases
https://github.com/seata/seata/releases
GitHub - 官方使用例子程序
https://github.com/seata/seata-samples
GitHub - Spring Cloud 快速集成 Seata
https://github.com/seata/seata-samples/blob/master/doc/quick-integration-with-spring-cloud.md
GitHub - 本文代码地址
https://github.com/1115675202/fruitbasket-litchi-distributed-transaction/tree/master/seata

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,这是一个比较常见的问题,下面是回答: Seata是一个开源的分布式事务解决方案,可以解决分布式事务的问题。Spring Boot是一个非常流行的Java框架,可以快速搭建基于Spring的应用程序。在Spring Boot中整合Seata可以实现业务回滚。 下面是整合Seata的步骤: 1. 引入Seata的依赖 在pom.xml中添加Seata的依赖: ``` <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>${seata.version}</version> </dependency> ``` 其中,${seata.version}是Seata的版本号。 2. 配置Seata 在Spring Boot的配置文件中添加Seata的配置: ``` spring: cloud: alibaba: seata: tx-service-group: your-service-group mybatis: mapper: mappers: io.seata.rm.datasource.xa.ResourceManagerMapper not-empty: true ``` 其中,tx-service-group是你的应用程序所属的分组名称。 3. 配置数据源 在Spring Boot的配置文件中配置数据源,并将数据源封装成Seata的代理数据源: ``` @Configuration public class DataSourceConfiguration { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } } ``` 4. 配置代理Bean 在Spring Boot的配置文件中配置Seata的代理Bean: ``` @Bean public GlobalTransactionScanner globalTransactionScanner() { return new GlobalTransactionScanner("your-service-group", "your-service"); } ``` 其中,your-service是你的应用程序的名称。 5. 编写业务代码 在业务代码中使用Seata的注解来管理事务: ``` @GlobalTransactional public void business() { // 业务逻辑 } ``` 其中,@GlobalTransactional注解表示这是一个分布式事务Seata会自动管理事务的提交和回滚。 综上所述,以上是整合Seata实现业务回滚的步骤。希望可以帮助你解决问题!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值