4
2
0
2

Seata分布式事务

一、理论基础

1.1 分布式服务的事务问题

在分布式系统下,一个业务跨越多个服务或数据源,每个服务都是一个分支事务,要保证所有分支事务最终状态一致,这样的事务就是分布式事务

image

1.2 CAP定理

- Consistency: 一致性
- Availability: 可用性
- Partition tolerance: 分区容错性

1. Consistency: 一致性

Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致

image

2. Availability: 可用性

Availability (可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝

image

3. Partition tolerance: 分区容错性

Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。

Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务

image

分布式系统节点通过网络连接,一定会出现分区问题(P)

当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足

1.3 BASE理论

BASE理论是对CAP的一种解决思路,包含三个思想:

- **Basically Available (基本可用)**:分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- **Soft State(软状态)**:在一定时间内,允许出现中间状态,比如临时的不一致状态。
- **Eventually Consistent(最终一致性)**:虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

而分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:

- AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现`最终一致`。
- CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成`强一致`。但事务等待过程中,处于弱可用状态。

1.4 分布式事务模型

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

image

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

二、Seata服务

2.1 Seata介绍

Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。官网地址:http://seata.io/,其中的文档、播客中提供了大量的使用说明、源码分析。

image

2.2 Seata架构

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

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

image

2.3 Seata的分布式事务解决方案(四种模式)

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

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

2.4 部署Seata的TC服务

参考资料:

seata的部署和集成

2.5 微服务集成Seata

1. 引入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>
    <exclusions>
        <!--版本较低,因此排除,根据自己的依赖版本进行选择-->
        <exclusion>
            <artifactId>druid</artifactId>
            <groupId>com.alibaba</groupId>
        </exclusion>
    </exclusions>
</dependency>
 <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.15</version>
</dependency>

2. 配置application.yml

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
        cluster-name: SH
# 添加下面的内容
seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    # 参考tc服务自己的registry.conf中的配置
    type: nacos
    nacos: # tc
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-tc-server # tc服务在nacos中的服务名称
      username: nacos
      password: nacos
  tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: # 事务组与TC服务cluster的映射关系
      seata-demo: SH

seata客户端获取tc的cluster名称方式: 以tx-group-service的值为key到vgroupMapping中查找

三、Seata的四种模式

四种模式对比:

image

3.1 XA模式

1. 原理

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

image

2. 工作模式

Seata的XA模式做了一些调整,但是大体相似

image

- RM一阶段工作:
 - 1.注册分支事务到TC
 - 2.执行分支业务SQL,但不提交
 - 3.报告执行状态到TC
- TC二阶段工作:
 - TC检测各分支事务的执行状态
  - a.如果`都`成功,通知所有RM `提交`事务
  - b.如果`有`失败,通知所有RM`回滚`事务
- RM二阶段工作:
 - 接收TC指令,`提交`或`回滚`事务

3.XA模式的优点

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

4. XA模式的缺点

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

5. 微服务实现XA模式

Seata的starter已经完成了XA模式的自动装配,实现非常简单,步骤如下:

1. 修改application.yml文件(每个参与事务的微服务),开启XA模式

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    # 参考tc服务自己的registry.conf中的配置
    type: nacos
    nacos: # tc
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-tc-server # tc服务在nacos中的服务名称
      username: nacos
      password: nacos
  tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: # 事务组与TC服务cluster的映射关系
      seata-demo: SH
  data-source-proxy-mode: XA # 开启数据源代理的XA模式  -- > 添加这里即可

2.给发起全局事务的入口方法添加 @GlobalTransactional注解,我这里是 OrderServiceImpl中的create方法

image

3.重启服务测试

3.2 AT模式

1. 原理

AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。

image

- 一阶段RM的工作:
 - 注册分支事务
 - `记录 undo-log (数据快照)`
 - 执行业务sql并`提交`
 - 报告事务状态
- 二阶段提交时RM的工作:
 - `删除 undo-log`即可
- 二阶段回滚时RM的工作:
 - 根据 `undo-log恢复`数据到更新前

image

2. AT模式与XA模式区别

1. XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
2. XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
3. XA模式强一致;AT模式最终一致

3. AT模式的写隔离

image

4. 优点和缺点

优点:

- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交

缺点:

- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式要好很多

5. 微服务实现AT模式

1. 导入sql,lock_table导入TC服务的数据库中,undo_log导入微服务相关数据库中

DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`  (
  `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
  `rollback_info` longblob NOT NULL COMMENT 'rollback info',
  `log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
  `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;

DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table`  (
  `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `branch_id` bigint(20) NOT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`row_key`) USING BTREE,
  INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

2. 修改application.yml文件,将食物模式修改为AT模式即可

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    # 参考tc服务自己的registry.conf中的配置
    type: nacos
    nacos: # tc
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-tc-server # tc服务在nacos中的服务名称
      username: nacos
      password: nacos
  tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: # 事务组与TC服务cluster的映射关系
      seata-demo: SH
#  data-source-proxy-mode: XA # 开启数据源代理的XA模式
  data-source-proxy-mode: AT # 开启数据源代理的AT模式  -- > 修改这一行即可

3. 重启服务测试

3.3 TCC模式

1. 原理

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

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

image

2. 工作模型

image

3. 总结

- TCC模式的每个阶段是做什么的?
 - 1.Try:资源检查和预留
 - 2.Confirm:业务执行和提交
 - 3.Cancel:预留资源的释放
- TCC的优点是什么?
 - 1.一阶段完成直接提交事务,释放数据库资源,性能好
 - 2.相比AT模型,无需生成快照,无需使用全局锁,性能最强
 - 3.不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
- TCC的缺点是什么?
 - 1.有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
 - 2.软状态,事务是最终一致
 - 3.需要考虑Confirm和Cancel的失败情况,做好幂等处理

4. 微服务利用TCC实现分布式事务

需求如下:

  • 修改account-service,编写try、confirm、cancel逻辑

  • try业务:添加冻结金额,扣减可用金额

  • confirm业务:删除冻结金额

  • cancel业务:删除冻结金额,恢复可用金额

  • 保证confirm、cancel接口的幂等性

  • 允许空回滚

  • 拒绝业务悬挂

1. 空回滚和业务悬挂

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

对于已经空回滚的业务,如果以后继续执行try,就永远不可能confirm或cancel,这就是业务悬挂。应当阻止执行空回滚后的try操作,避免悬挂

image

2. 业务分析

为了实现空回滚、防止业务悬挂,以及幂等性要求。我们必须在数据库记录冻结金额的同时,记录当前事务id和执行状态,为此我们设计了一张表:

DROP TABLE IF EXISTS `account_freeze_tbl`;
CREATE TABLE `account_freeze_tbl`  (
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '事务id',
  `user_id` varchar(255) 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 '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

image

3. 声明 TCC接口

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

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 {

    /**
     * Try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法
     * @TwoPhaseBusinessAction 两阶段业务行为
     * @param userId 用户id
     * @param money  钱
     */
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    void  deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                 @BusinessActionContextParameter(paramName = "money") int money);

    /**
     * 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致
     *
     * @param businessActionContext 上下文,可以传递try方法的参数
     * @return boolean
     */
    boolean confirm(BusinessActionContext businessActionContext);

    /**
     * 二阶段回滚方法,要保证与rollbackMethod一致
     *
     * @param businessActionContext 业务操作环境
     * @return boolean
     */
    boolean cancel(BusinessActionContext businessActionContext);
}

4. 实现方法

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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/4
 */
@Service()
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {

    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private AccountFreezeMapper accountFreezeMapper;
    /**
     * 进行资源判断和预留
     *
     * 扣除
     *
     * @param userId 用户id
     * @param money  钱
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deduct(String userId, int money) {
        // 0. 获取事务id
        String xid = RootContext.getXID();

        // 1. 判断 freeze中是否有冻结记录,如果有,则一定是try过了,拒绝业务
        AccountFreeze oldFreeze = accountFreezeMapper.selectById(xid);
        if (oldFreeze != null) {
            // try过了,拒绝业务
            return;
        }

        // 1. 扣减可用余额
        accountMapper.deduct(userId, money);
        // 2. 记录冻结金额,事务的状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);
        // 事务id
        freeze.setXid(xid);
        accountFreezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext businessActionContext) {
        // 1. 获取事务id
        String xid = businessActionContext.getXid();
        // 2.根据id删除冻结记录
        int count = accountFreezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext businessActionContext) {
        // 0. 查询冻结金额
        String userId = businessActionContext.getActionContext().get("userId").toString();
        String xid = businessActionContext.getXid();
        AccountFreeze accountFreeze = accountFreezeMapper.selectById(xid);
        // 1. 空回滚判断,判断 accountFreeze 是否为null,为null的话说明try没执行,需要空回滚
        if (accountFreeze == null) {
            // 空回滚
            AccountFreeze freeze = new AccountFreeze();
            freeze.setUserId(userId);
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            // 事务id
            freeze.setXid(xid);

            accountFreezeMapper.insert(freeze);
            return true;
        }

        // 2.幂等性判断
        if (accountFreeze.getState() == AccountFreeze.State.CANCEL) {
            // 已经处理过CANCEL了,无需再次处理
            return true;
        }

        // 1. 恢复可用余额
        accountMapper.refund(accountFreeze.getUserId(), accountFreeze.getFreezeMoney());
        // 2. 将冻结金额清零,状态改为 CANCEL
        accountFreeze.setFreezeMoney(0);
        accountFreeze.setState(AccountFreeze.State.CANCEL);
        int count = accountFreezeMapper.updateById(accountFreeze);
        return count == 1;
    }
}

5. 修改调用的service,然后重启测试

3.4 SAGA模式

1. 原理

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

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

image

2. 总结

优点:

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

缺点:

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

四、TC的异地多机房容灾架构

TC服务作为Seata的核心服务,一定要保证高可用和异地容灾。

image

4.1 部署

具体参考 : seata的部署和集成

posted @ 2023-02-04 16:13  CoderTL  阅读(62)  评论(0编辑  收藏  举报