• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

分布式事务Seata一站式学习

武飞扬头像
阿伟在自律
帮助1

分布式事务模型

解决分布式事务,各个子系统之间必须能感知到彼此的事务状态,才能保证状态一致,因此需要一个事务协调者来协调每一个事务的参与者(子系统事务)
这里的子系统事务,称为分支事务。有关联的各个分支事务在一起称为全局事务

学新通

解决分布式事务的思想和模型

  • 全局事务:整个分布式事务
  • 分支事务:分布式事务中包含的每个子系统的事务
  • 最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据
  • 强一致思想:各分支事务执行完业务不要提交,等待彼此结果。而后统一提交或回滚

Seata

Seata事务管理中有三个重要的角色:

  • Tc(Transaction Coordinator)- 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚
  • TM(Transaction Manager)-事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
  • RM(Resource Manaer)-资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
    学新通

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚,在 Seata 中,分布式事务的执行流程如下:

  1. TM 开启分布式事务(TM 向 TC 注册全局事务记录);

  2. 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );

  3. TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);

  4. TC 汇总事务信息,决定分布式事务是提交还是回滚;

  5. TC 通知所有 RM 提交/回滚 资源,事务二阶段结束。

  6. TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。

Seata提供了四种不同的分布式事务解决方案

  • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
  • TCC模式:最终一致的分阶段事务模式,有业务侵入
  • AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
  • SAGA模式:长事务模式,有业务侵入

配置Seata

  1. 在seata的安装目录bin下修改配置文件conf,可以看资料
  2. idea引入seata依赖
<!--        seata依赖-->
       <dependency>
           <groupId>com.alibaba.cloud</groupId>
           <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
           <exclusions>
               <!--版本较低,1.3.0,因此排除-->
               <exclusion>
                   <artifactId>seata-spring-boot-starter</artifactId>
                   <groupId>io.seata</groupId>
               </exclusion>
           </exclusions>
       </dependency>
       <!--seata starter 采用1.4.2版本-->
       <dependency>
           <groupId>io.seata</groupId>
           <artifactId>seata-spring-boot-starter</artifactId>
           <version>${seata.version}</version>
       </dependency>
学新通
  1. 修改yaml
seata:
  registry:  # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-server # tc服务在nacos中的服务名称
      username: nacos
      password: nacos
  tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: # 事务组与TC服务cluster的映射关系
      seata-demo: default #集群地址

这里的事务组是兜了一圈,多个微服务在同一个事务组内。详情后面说

  tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: # 事务组与TC服务cluster的映射关系
      seata-demo: default

XA模式

XA规范是X/0pen 组织定义的分布式事务处理(DTP,DistributedTransaction Processing)标准,XA规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对XA规范提供了支持。实现了强一致性的特性

学新通


seata的XA模式

XA工作流程:首先由TM开启全局事务控制,随后调用分支事务,在每个微服务中先由RM注册分支事务,然后开始具体的执行sql,但是不提交告诉TC事务的状态,如果全都执行成功,则告诉RM事务可以提交,如果有一个出错,则通知RM事务回滚

学新通

RM一阶段的工作:

  1. 注册分支事务到TC
  2. 执行分支业务sql但不提交
  3. 报告执行状态到TC

TC二阶段的工作:

  1. TC检测各分支事务执行状态
    • 如果都成功,通知所有RM提交事务
    • 如果有失败,通知所有RM回滚事务

RM二阶段的工作:

  • 接收TC指令,提交或回滚事务

总结

XA模式的优点是什么?

  • 事务的强一致性,满足ACID原则。
  • 常用数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

案例

  1. 在每个参与事务的微服务的yaml中配置:
seata:
  data-source-proxy-mode: XA
  1. 在发起全局事务的入口方法添加@GlobalTransactional注解
@Override
@GlobalTransactional
public Long create(Order order) {
    // 创建订单
    orderMapper.insert(order);
    try {
        // 扣用户余额
        accountClient.deduct(order.getUserId(), order.getMoney());
        // 扣库存
        storageClient.deduct(order.getCommodityCode(), order.getCount());

    } catch (FeignException e) {
        log.error("下单失败,原因:{}", e.contentUTF8(), e);
        throw new RuntimeException(e.contentUTF8(), e);
    }
    return order.getId();
}
学新通

AT模式

AT模式工作流程:首先由TM通知TC开启全局事务,随后调用分支,由每个微服务的RM注册分支事务,然后开始执行业务sql,执行的时候会被拦截这个sql,生成一个快照文件,并且提交事务,全部服务提交完后RM向TC报告事务状态,如果有报错,则根据快照文件回滚,然后删除快照

学新通

AT跟XA有什么区别?

  • AT模式分支事务会直接提交事务,不锁定资源。XA模式分支事务会等全部分支没问题才会提交事务,在这期间会锁定资源
  • AT根据快照文件回滚数据,XA根据数据库机制回滚数据
  • AT模式保证最终一致性,XA模式保证强一致性

AT模式的脏写问题

学新通

解决办法

学新通

首先事务1先获取DB锁,保存快照,执行sql,提交事务前获取全局锁(这个全局锁是seata管理的),提交事务释放DB锁,此时全局锁还没有释放,还在事务1上。此时事务2进来了,获取到了DB锁,保存快照执行sql,提交事务前获取全局锁,但是获取不到,要等事务1释放全局锁。如果此时事务1需要回滚,那么就需要获取DB锁通过快照文件恢复数据,而DB锁在事务2上,就造成了死锁。seata对这种情况做了处理,如果获取全局锁失败会重试30次,间隔10ms,共计300ms,然后不再获取全局锁。事务2获取全局锁失败任务超时回滚并释放DB锁,事务1获取DB锁回滚数据。如果事务1不需要回滚,则没有后面死锁这一段了。

思考:这种全局锁一样锁定了资源,跟XA有啥区别?即XA不提交事务是DB锁锁定了,AT全局锁锁了资源

AT全局锁是由seata管理的,而XA的DB锁是数据库的。全局锁锁定,不是这个seata管理的事务也可以操作数据库,而DB锁任何事务都不能操作

思考:使用AT的时候,如果非seata管理的事务1跟seata管理的事务2同时操作事务,且事务2回滚,会不会造成脏写,即事务1丢失更新了

  1. 情况很少,大多数情况下不会造成二阶段事务回滚

  2. 分布式事务并发量低,很少巧合刚好事务提交了释放完锁,此时非seata事务进来获取DB

  3. 尽可能避免多个不一样的事务操作一个字段

思考:如果真的发生了,怎么处理?

当事务1一阶段完成后,普通事务2进来了提交事务后,事务1需要回滚数据,此时其实是有两份快照的,一份是事务1开始的快照,一份是一阶段事务提交前sql执行后的快照。回滚的时候seata判断这个一阶段事务提交时的快照90是不是正确的,如果不是说明在提交事务之后,回滚之前是有其他事务进来操作了数据的。就需要人工干预

学新通

总结

AT模式的优点:

  1. 一阶段执行完sql直接提交事务,不用等到TC通知统一全部提交事务,中间减少了对资源的占用,性能好
  2. 利用全局锁实现读写隔离
  3. 代码0侵入

AT模式的缺点:

  1. 两个阶段之间属于软状态,属于最终一致
  2. 快照功能会影响性能,但是比XA模式要好很多

案例

seata-at.sql:其中lock_table导入到TC服务关联的数据库,undo_log表导入到微服务关联的数据库:

  1. 在每个参与事务的服务的yaml中修改
  data-source-proxy-mode: AT #默认就是AT
  1. 准备两张表在数据库

    • lock_table导入到TC服务关联的数据库(总的seata数据库)
    • undo_log表导入到微服务关联的数据库
    • 规定好的,在file.conf里面
     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 = "mysql"
        password = "mysql"
        minConn = 5
        maxConn = 100
        globalTable = "global_table"
        branchTable = "branch_table"
        lockTable = "lock_table"
        queryLimit = 100
        maxWait = 5000
      }
    
    学新通

TCC模式

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

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

举个例子

学新通

TCC工作流程

学新通

TM通知TC开启全局事务,随后调用分支事务,由RM注册分支事务,RM资源预留Try直接提交事务,向TC报告事务状态,如果没问题就提交全局事务,有问题就回滚

总结

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

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

TCC的优点是什么?

  • 性能最好,不会生成快照文件,不用使用全局锁
  • 一阶段完成直接提交事务,跟AT模式的一阶段很像,但是AT要加锁
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务性数据库

TCC的缺点是什么?

  • 人工编写三个方法,有代码侵入
  • 软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理。有可能失败的时候重试,导致多补偿的情况,要考虑健壮性

其实TCC并不适用于全部事务,比如新增操作,没法预留资源


TCC空回滚和业务悬挂

学新通

空回滚

如果调用分支事务1锁定了资源(Try),此时分支事务2网络阻塞,一直不能锁定资源,超时后TM通知TC全部回滚,那事务1回滚没问题,因为已经try过了,但是事务2还没有try就要回滚了。就需要空回滚,判断事务2有没有try过

业务悬挂

上面的业务继续。空回滚后,即全部回滚了,此时事务2网络好了,又要重新去锁定资源try,这个时候整个事务已经结束了全部回滚了就已经没意义了,等于说事务2执行了一半的业务没用了,就是业务悬挂。需要判断当前事务有没有回滚过,如果回滚过说明事务结束了。

来说一下关于业务悬挂的问题吧

在try方法执行的过程中,如果发生了超时,就会容易出现问题。是分情况的,

  • 如果超时的时候已经执行了try,即已经锁定了资源,那么一开始通过判断冻结资源可以防止业务悬挂。
  • 如果超时的时候还没有执行try的核心,即没有锁定资源,那么判断冻结资源就不能防止业务悬挂,因为超时的时候没有冻结资源,这个时候会直接回滚,然后会出现业务悬挂的情况。

声明TCC接口

@LocalTCC //TCC注解
public interface AccountTCCService {
    /**
     * @TwoPhaseBusinessAction 表明这个是try方法,name是try方法名称,必须一致
     * @BusinessActionContextParameter 有这个注解标识的参数会放到BusinessActionContext这个上下文对象里,可以通过这个上下文对象获取到参数
     * @param userId
     * @param money
     */
    @TwoPhaseBusinessAction(name="deduct",commitMethod = "confirm",rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money") int money);

    boolean confirm(BusinessActionContext context);

    boolean cancel(BusinessActionContext context);
}
学新通

案例

改造account-service服务,利用TCC实现分布式事务,需求如下:

  • 修改account-service,编写try、confirm、cancel逻辑
  • try业务:添加冻结金额,扣减可用金额
  • confirm业务:删除冻结金额
  • cancel业务:删除冻结金额,恢复可用金额
  • 保证confirm、cancel接口的幂等性
  • 允许空回滚
  • 拒绝业务悬挂

学新通

@Service
public class AccountTCCServiceImpl implements AccountTCCService {
    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper accountFreezeMapper;
    /**
     * try方法
     * @param userId
     * @param money
     */
    @Override
    public void deduct(String userId, int money) {
//        获取事务id
        String xid = RootContext.getXID();
//        先判断是否业务悬挂,如果freeze有冻结记录,说明一定做过cancel,拒绝业务
        if (accountFreezeMapper.selectById(xid)!=null) {
            return;
        }
//        1.先判断余额够不够
        LambdaQueryWrapper<Account> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Account::getUserId,userId);
        Account account = accountMapper.selectOne(wrapper);
        if (account.getMoney()<money) {
            throw new RuntimeException("余额不足");
        }
//        2.扣减account表的金额
        accountMapper.deduct(userId,money);
//        3.记录冻结金额,事务状态,写入accountFreeze
        AccountFreeze accountFreeze = new AccountFreeze();
        accountFreeze.setUserId(userId);
        accountFreeze.setFreezeMoney(money);
        accountFreeze.setXid(xid);
        accountFreeze.setState(AccountFreeze.State.TRY);
        accountFreezeMapper.insert(accountFreeze);
    }

    /**
     * 提交事务
     * @param context
     * @return
     */
    @Override
    public boolean confirm(BusinessActionContext context) {
//        1.先获取事务id
        String xid = context.getXid();
//        2.删除冻结记录
        int count = accountFreezeMapper.deleteById(xid);
        return count == 1;
    }

    /**
     * 回滚
     * @param context
     * @return
     */
    @Override
    public boolean cancel(BusinessActionContext context) {
        String xid = context.getXid();
        int money =(int) context.getActionContext("money");
        String userId =context.getActionContext("userId").toString();
        AccountFreeze accountFreeze = accountFreezeMapper.selectById(xid);
//        1.判断有没有扣过,即空回滚判断,如果为空,说明没有try,需要空回滚
        if (accountFreezeMapper.selectById(xid) == null) {
            accountFreeze = new AccountFreeze();
            accountFreeze.setUserId(userId);
            accountFreeze.setFreezeMoney(0);
            accountFreeze.setXid(xid);
            accountFreeze.setState(AccountFreeze.State.CANCEL);
            return true;
        }
//        幂等判断,判断状态是不是cancel,如果是cancel说明已经处理过了
//        如果不做幂等判断,有可能这个方法会一直调用,导致一直恢复金额
        if (accountFreeze.getState() == AccountFreeze.State.CANCEL) {
            return true;
        }
//        2.恢复扣的钱
        accountMapper.refund(userId,money);
//        3.将冻结金额清零,状态改成cancel
        accountFreeze.setState(AccountFreeze.State.CANCEL);
        accountFreeze.setFreezeMoney(0);
        int count = accountFreezeMapper.updateById(accountFreeze);
        return count==1;
    }
}
学新通
@LocalTCC //TCC注解
public interface AccountTCCService {
    /**
     * @TwoPhaseBusinessAction 表明这个是try方法,name是try方法名称,必须一致
     * @BusinessActionContextParameter 有这个注解标识的参数会放到BusinessActionContext这个上下文对象里,可以通过这个上下文对象获取到参数
     * @param userId
     * @param money
     */
    @TwoPhaseBusinessAction(name="deduct",commitMethod = "confirm",rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money") int money);

    boolean confirm(BusinessActionContext context);

    boolean cancel(BusinessActionContext context);
}
学新通

Saga模式

saga模式是SEATA提供的长事务解决方案。也分为两个阶段

  • 一阶段:直接提交本地事务
  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

Saga模式优点:

  • 事务参与者可以基于事件驱动实现异步调用,吞吐高
  • 一阶段直接提交事务,无锁,性能好
  • 不用编写TCC中的三个阶段,实现简单

缺点:

  • 软状态持续时间不确定,时效性差
  • 没有锁,没有事务隔离,会有脏写

四种模式对比

学新通

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhficgje
系列文章
更多 icon
同类精品
更多 icon
继续加载