分布式事务(二)Seata AT模式 - Spring Cloud 微服务 添加 AT 分布式事务

分布式事务(二)Seata AT 模式

Seata AT 事务方案

Seata 的 AT 模式(Automatic Transaction)是一种无侵入的分布式事务解决方案。

业务场景:订单系统

当用户下订单时,执行以下三步流程:

  • 订单系统保存订单
  • 订单系统调用库存服务,减少商品库存
  • 订单系统调用账户服务,扣减用户金额

这三步要作为一个整体事务进行管理,要么整体成功,要么整体失败。

Seata AT 基本原理

Seata AT 事务分两个阶段来管理全局事务:

第一阶段: 执行各分支事务

微服务系统中,各服务之间无法相互感知事务是否执行成功,这时有一个专门的服务,来协调各个服务的运行状态。这个服务称为 TC(Transaction Coordinator),事务协调器。

  • 订单系统开始执行保存订单之前,首先启动 TM(Transaction Manager,事务管理器),由 TM 向 TC 申请开启一个全局事务。
  • 这时TC会产生一个全局事务ID,称为 XID,并将 XID 传回 TM,这样就开启了 全局事务
  • 全局事务开启后,开始执行创建订单的业务。首先执行保存订单,这时会先启动一个 RM(Resource Manager,资源管理器),并将 XID 传递给 RM。
  • RM 负责对分支事务(即微服务的本地事务)进行管理,并与 TC 通信,上报分支事务的执行状态、接收全局事务的提交或回滚指令。
  • RM 首先会使用 XID 向 TC 注册分支事务,将分支事务纳入对应的全局事务管辖。
  • 现在可以执行保存订单的分支事务了。一旦分支事务执行成功,RM 会上报事务状态。
  • TC 收到后,会将该状态信息传递到 TM,到此,保存订单过程结束。
  • 下面是调用库存服务,减少商品库存,与订单的执行过程相同。
    首先调用库存服务,启动 RM,并传递 XID。
  • 库存服务的 RM 使用 XID 向 TC 进行注册,纳入全局事务管辖。
  • 执行本地事务成功后上报状态,TC会将状态发送给TM。
  • 相同的,完成账户分支事务。

第二阶段: 控制全局事务最终提交或回滚

  • 现在,TM(全局事务管理器)收集齐了全部分支事务的成功状态,它会进行决策,确定全局事务成功,向 TC 发送全局事务的提交请求。
  • 然后,TC 会向所有 RM 发送提交操作指令,RM 会完成最终提交操作。
  • 到此,全局事务全部提交完成。

第二阶段:控制全局事务最终回滚

  • 上面是全局事务执行成功的情况,下面再来看看事务执行失败的情况。
    假设订单业务执行过程中,扣减账户金额这一步分支事务执行失败,那么失败状态对TC上报,然后再发送给TM。
  • TM 会进行决策,确定全局事务失败,向 TC 发送全局事务的回滚请求。
  • 然后,TC 会向所有 RM 发送回滚操作指令,RM 会完成最终回滚操作。

Seata Server - TC 全局事务协调器

AT 事务的三个角色:
TC(事务协调器)、TM(事务管理器)和RM(资源管理器)。
其中 TM 和 RM 是嵌入在业务应用中的,而 TC 则是一个独立服务。

启动事务协调器(TC)-- seata server

解压 “课前资料/分布式事务/seata-server-1.3.zip”

在这里插入图片描述

更改三个配置文件

  • registry.conf – 向注册中心注册(conf 目录下)

    更改注册方式:eureka

    更改 eureka 应用名:seata-server

在这里插入图片描述

  • file.conf – seata 保存日志(conf 目录下)

    更改存储模式:db

    更改 db 配置:驱动改为 cj 包、用户名密码改为自己的、url 参数添加时区。

在这里插入图片描述

  • seata-server.bat 内存配置(bin目录下)

    更改 85 行 Xmx/Xms/Xmn 参数分别为 256/256/128

在这里插入图片描述

执行 seata-server.bat 启动,eureka 注册中心有 SEATA-SERVER 服务名即可。

  • 若无法启动,JAVA_HOME 和 PATH的配置
  • 若无法启动,若新版本 JDK,有的启动参数不支持,按照报错删掉这些参数即可。
  • 在 cmd 中不能选中或指示白点,否则程序会变为挂起状态,无法使用。

在这里插入图片描述

添加 Seata AT 事务

在业务模块中,添加 Seata AT 事务

Order 订单模块

添加 seata 依赖:

  • 把 order-parent 的 pom 文件中的注释部分打开。

  • 重启 三个业务模块,会发现启动不了:
    NotSupportYetException: config type can not be null 报错。
    原因是没有添加配置,属于正常情况。

修改 order 订单服务三个配置文件

  • application.yml – 事务组命名

    添加 cloud.alibaba.seata.tx-service-group 配置,命名为 order_tx_group。

spring:  
  cloud:
    alibaba:
      seata:
        # 三个模块 order,storage,account 都要在这同一个事务组中
        tx-service-group: order_tx_group
  • registry.conf --注册中心地址,用来发现事务协调器。

    从笔记复制即可。

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "eureka"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    # application = "default"
    # weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
    password = ""
    cluster = "default"
    timeout = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

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

  nacos {
    serverAddr = "localhost"
    namespace = ""
    group = "SEATA_GROUP"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

  • file.conf – 事务组对应使用哪个协调器

    从笔记复制即可。查看 36 行配置,当前事务组,对应使用哪个协调器。
    事务组命名要与 file.conf 中 36 行的参数一致。

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #transaction service group mapping
  # order_tx_group 与 yml 中的 “tx-service-group: order_tx_group” 配置一致
  # “seata-server” 与 TC 服务器的注册名一致
  # 从eureka获取seata-server的地址,再向seata-server注册自己,设置group
  # 当前事务组,对应使用哪个协调器
  vgroupMapping.order_tx_group = "seata-server"
  #only support when registry.type=file, please don't set multiple addresses
  order_tx_group.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}

创建数据源代理

  • 在 order 包下创建自动配置类 DSAutoConfiguration:

    在类上添加配置类注释 @Configuration。

  • 创建原始数据源对象(通过方法):

    创建 DataSource 数据源对象方法 dataSource,返回一个 Hikari 数据源对象,方法上添加 @Bean 交给 Spring 容器管理,添加 @ConfigurationProperties 注解并指定前缀,这里的前缀是指 datasource 在 application.yml 中的具体路径(spring.datasource)。(这里也可以返回一个阿里的数据源对象,DruidDataSource)

@Configuration
public class DSAutoConfiguration {
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource dataSource(){
        return new HikariDataSource();
    }
}

注意这里的引包,DataSource 是要引 javax.sql 包。

  • 修改 application.yml 中 datasource 属性:

    因为 Hikari 需要一个 jdbcUrl 的属性,所以我们添加它,值和上面的 url 一致。

jdbcUrl: ${spring.datasource.url}
  • 创建数据源代理对象:

    创建一个方法 dataSourceProxy,参数为数据源对象,返回一个数据源代理对象,方法上添加 @Bean 交给 Spring 容器注入。添加 @Primary 首选项

    @Primary
    @Bean
    public DataSource dataSourceProxy(DataSource ds){
        return new DataSourceProxy(ds);
    }
  • 启动类注解添加配置

    在 @SpringBootApplication 注解后面添加 exclude = DataSourceAutoConfiguration.class

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

在 OrderServiceImpl 业务方法上,添加注解

  • @GlobalTransactional – 用来启动全局事务,只在第一个模块添加
  • @Transactonal – 控制本地事务

启动测试

  • 为了进行测试,把业务方法类中的远程调用业务先注释

  • 按顺序启动服务:

    Eureka
    Seata Server
    Easy Id Generator
    Order

  • 调用保存订单,地址:
    http://localhost:8083/create?userId=1&productId=1&count=10&money=100

  • 观察控制台,看到全局事务和订单的分支事务已经启动,并可以看到全局事务ID(XID)和分支事务ID(Branch ID)

在这里插入图片描述

Account 和 Storage 业务模块

修改 storage 和 account 业务相关配置

  • 修改 application.yml 文件,添加事务组相关配置(jdbcUrl 也包括)。
  • 将 order 订单服务的两个 conf 配置文件直接复制到业务目录下。
  • 若 conf 配置文件未生效,可 build、compile、重启解决。

创建 seata 数据源代理

和 order 订单服务完全相同,可直接复制 DSAutoConfiguration。

排除 Spring 默认配置

在启动类注解添加参数 (exclude = DataSourceAutoConfiguration.class)。

在业务方法上添加注解

ServiceImpl 实现类方法上添加 @Transactional 注解,控制本地事务。

将 OrderServiceImpl 实现类方法上注释的部分解开。

启动 storage 项目进行测试

按顺序启动项目:

  • Eureka
  • Seata Server
  • Easy Id Generator
  • Storage
  • Account
  • Order

调用保存订单,地址:

http://localhost:8083/create?userId=1&productId=1&count=10&money=100

订单会调用库存和账户,这三个服务会分别启动一个分支事务,三个分支事务一起组成一个全局事务。

观察三个项目的控制台都有 Seata AT 事务的日志即可。

在这里插入图片描述

然后观察数据库中的订单表、库存表和账户表。

测试出现异常回滚

调整 Account 类,使得金额不足时出错。

Mapper 添加查询方法 selectByUserId 返回一个 Account 对象,参数为 userId。

Mapper.xml 添加 SQL 语句:

<select id="selectByUserId" resultMap = "BaseResultMap">
    select * from account where user_id = #{userId};
</select>

在 ServiceImpl 实现类中 decrease 方法中增加逻辑:

判断账户可用余额 residue 是否小于金额 money,若小于则抛出异常 “可用金额不足!”

	public void decrease(Long userId, BigDecimal money) {
    Account account = accountMapper.selectByUserId(userId);
    /*
    BigDecimal a    BigDecimal b
    a.compareTo(b)
        返回值
        正数,表示a大
        负数,表示a小
        0,表示相同
     */
    if (account.getResidue().compareTo(money) < 0){
        throw new RuntimeException("可用金额不足!");
    }
    accountMapper.decrease(userId,money);
}

重启测试

访问:
http://localhost:8083/create?userId=1&productId=1&count=10&money=400

查看数据库表 order、storage 和 account,如果执行成功会新增订单、减少库存、扣减金额,如果执行失败则数据没有变化,被回滚了。

失败时,在 order 和 storage 控制台可以看到回滚日志。

storage 控制台:
在这里插入图片描述
account 控制台:

在这里插入图片描述

order 控制台:

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值