Seata AT模式--Spring Cloud+ AT 分布式事务

一.Seata介绍

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

2019 年 1 月,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback),和社区一起共建开源分布式事务解决方案。Fescar 的愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题。

Fescar 开源后,蚂蚁金服加入 Fescar 社区参与共建,并在 Fescar 0.4.0 版本中贡献了 TCC 模式。

为了打造更中立、更开放、生态更加丰富的分布式事务开源社区,经过社区核心成员的投票,大家决定对 Fescar 进行品牌升级,并更名为 Seata,意为:Simple Extensible Autonomous Transaction Architecture,是一套一站式分布式事务解决方案。

Seata 融合了阿里巴巴和蚂蚁金服在分布式事务技术上的积累,并沉淀了新零售、云计算和新金融等场景下丰富的实践经验,但要实现适用于所有的分布式事务场景的愿景,仍有很长的路要走。

Seata AT事务方案

Seata 的 AT 模式(Automatic Transaction)是一种无侵入的分布式事务解决方案。下面结合具体业务场景来分析其执行的原理。

业务场景

订单系统
在这里插入图片描述
当用户下订单时,执行以下三步流程:

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

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

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 AT具体工作机制

以上了解了 Seata AT 的基本原理、工作流程,那么 Seata 具体是如何实现全局事务的提交和回滚操作呢?下面来分析 Seata 的具体工作机制。

第一阶段:执行分支事务

以全面订单业务中的库存服务为例,库存表中存在一条商品的库存信息:
在这里插入图片描述
现在要执行业务操作减少库存,从50件减少到40件:
在这里插入图片描述
执行修改库存业务操作前, 会先取出旧的库存信息:
在这里插入图片描述
现在可以修改库存了:
在这里插入图片描述
接着,取出更新后的新数据:
在这里插入图片描述
接下来,会把旧数据和新数据合并起来,保存到一个事务回滚日志表:undo_log表:
在这里插入图片描述
至此,第一阶段,分支事务完成,将状态上报给TC:
在这里插入图片描述

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

假如全局事务失败,那么第一阶段已提交的分支事务要执行回滚操作。

首先会收到来自 TC 的全局事务回滚指令:

在这里插入图片描述
接下来,根据事务回滚日志(undo_log)表的记录,将商品恢复成旧的库存数据:

在这里插入图片描述

然后删除事务日志,最终完成第二阶段回滚操作:
在这里插入图片描述

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

上面是全局事务回滚操作。如果全局事务成功,要完成最终提交,AT模式最终提交操作非常简单,只需要删除日志数据即可。

首先接收到 TC 的全局事务提交指令:
在这里插入图片描述
接着,直接删除事务日志,就完成了第二阶段提交操作:
在这里插入图片描述

二.Seata AT模式+AT分布式事务

项目源码:

无事务版案例代码可以在这里下载:

TC(事务协调器)、TM(事务管理器)和RM(资源管理器),其中 TM 和 RM 是嵌入在业务应用中的,而 TC 则是一个独立服务。
在这里插入图片描述
Seata Server 就是 TC,直接从官方仓库下载启动即可,下载地址:

1. 解压缩
2. 修改三个配置文件
1.registry.conf–向注册中心注册

在这里插入图片描述
Seata Server 要向注册中心进行注册,这样,其他服务就可以通过注册中心去发现 Seata Server,与 Seata Server 进行通信。

Seata 支持多款注册中心服务:nacos 、eureka、redis、zk、consul、etcd3、sofa。

我们项目中要使用 eureka 注册中心,eureka服务的连接地址、注册的服务名,这需要在 registry.conf 文件中进行配置:

在这里插入图片描述

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  # 这里选择 eureka 注册配置
  type = "eureka"

  nacos {
	......
  }

  # eureka的注册配置
  eureka {
    # 注册中心地址
    serviceUrl = "http://localhost:8761/eureka"
    # 注册的服务ID
    application = "seata-server"
    weight = "1"
  }
  
  redis {
	......
  }
  ......

2.file.conf

Seata 需要存储全局事务信息、分支事务信息、全局锁信息,这些数据存储到什么位置?

针对存储位置的配置,支持放在配置中心,或者也可以放在本地文件。Seata Server 支持的配置中心服务有:nacos 、apollo、zk、consul、etcd3。

这里我们选择最简单的,使用本地文件,这需要在 registry.conf 配置文件中来指定:

......

config {
  # file、nacos 、apollo、zk、consul、etcd3
  # 在这里选择使用本地文件来保存配置
  type = "file"


......

  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  
  file {
    # 在这里设置配置文件的文件名
    name = "file.conf"
  }
}

file.conf 中对事务信息的存储位置进行配置,存储位置支持:file、db、redis。

这里我们选择数据库作为存储位置,这需要在 file.conf 中进行配置:

store {
  ## store mode: file、db、redis
  # 这里选择数据库存储
  mode = "db"

  ## file store property
  file {
  	......
  }

  # 数据库存储
  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"

	# 数据库连接配置
    url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"
    user = "root"
    password = "root"
    minConn = 5
    maxConn = 30

	# 事务日志表表名设置
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"

    queryLimit = 100
    maxWait = 5000
  }

  ## redis store property
  redis {
  	......
  }
}

在这里插入图片描述
在这里插入图片描述

3.启动参数设置

启动文件:seata-server.bat

seata-server.bat–使用的内存默认是2G,测试环境把内存改小:256m
在这里插入图片描述

3. 启动seata-server.bat启动协调器

双击 seata-server.bat 启动 Seata Server。

查看 Eureka 注册中心 Seata Server 的注册信息:
在这里插入图片描述

  • 必须用jdk1.8
  • 不能关闭命令窗口
  • 窗口中的内容不能选中,如果选中,窗口中的应用会挂起(暂停)

三.订单模块添加Seata AT事务

在这里插入图片描述
订单调用库存和账户,我们先从前面的订单开始。

在订单项目中要启动全局事务,还要执行订单保存的分支事务

1. 父项目中添加seata依赖

order-parent 的 pom.xml 文件中有一段注释掉的 seata 依赖,现在可以打开它了:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.tedu</groupId>
    <artifactId>order-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>order-parent</name>

    <modules>
        <module>account</module>
        <module>storage</module>
        <module>order</module>
    </modules>


    <properties>
        <mybatis-plus.version>3.3.2</mybatis-plus.version>
        <druid-spring-boot-starter.version>1.1.23</druid-spring-boot-starter.version>
        <seata.version>1.3.0</seata.version>
        <spring-cloud-alibaba-seata.version>2.0.0.RELEASE</spring-cloud-alibaba-seata.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-spring-boot-starter.version}</version>
        </dependency>


        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-seata</artifactId>
            <version>${spring-cloud-alibaba-seata.version}</version>
            <exclusions>
                <exclusion>
                    <artifactId>seata-all</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>${seata.version}</version>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>


2. order中配置三个配置文件

application.yml – 事务组的组名
spring:
  application:
    name: order
  datasource:
    url: jdbc:mysql:///seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    jdbcUrl: ${spring.datasource.url}
  cloud:
    alibaba:
      seata:
        # 当前模块在哪个事务组中
        tx-service-group: order_tx_group

server:
  port: 8083
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.tedu.order.entity
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    cn.tedu.order.mapper: debug

ribbon:
  MaxAutoRetriesNextServer: 0  # 默认1

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 – 上面的事务组,使用哪个协调器
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


  # order_tx_group 事务组,对应使用哪个协调器
  # seata-server 是注册表中的服务id
  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
  }
}

3. 新建自动配置类,创建DatasourceProxy数据源代理对象

Seata AT 事务对业务代码无侵入,全自动化处理全局事务,其功能是靠 Seata 的数据源代理工具实现的。

这里我们创建 Seata 的数据源代理,并排除 Spring 默认的数据源。

package cn.tedu.order;

import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
public class DSPAutoConfiguration {
    // 新建原始数据源对象
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }

    // 新建数据源代理对象
    @Primary  // 首选对象
    @Bean
    public DataSource dataSourcePoxy(DataSource ds) {
        return new DataSourceProxy(ds);
    }
}

主程序中排除Springboot 的默认数据源:

package cn.tedu.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@MapperScan("cn.tedu.order.mapper")
// 禁用spring默认的数据源配置
// 只使用自定义配置 DSPAutoConfiguration
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class OrderApplication {

	public static void main(String[] args) {
		SpringApplication.run(OrderApplication.class, args);
	}

}

4. 在业务方法上添加事务注解

- 本地事务@Transactional
- seata启动全局事务@GlobalTransactional(只在第一个模块上添加)
package cn.tedu.order.service;

import cn.tedu.order.entity.Order;

import cn.tedu.order.fegin.AccountClient;
import cn.tedu.order.fegin.EasyIdClient;
import cn.tedu.order.fegin.StorageClient;
import cn.tedu.order.mapper.OrderMapper;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Random;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private EasyIdClient easyIdClient;
    @Autowired
    private AccountClient accountClient;
    @Autowired
    private StorageClient storageClient;


    @GlobalTransactional //启动全局事务,只在第一个模块上添加
    @Transactional
    @Override
    public void create(Order order) {
        // 远程调用发号器,生成订单id
        String s = easyIdClient.nextId("order_business");
        Long id = Long.valueOf(s);

        order.setId(id);
        orderMapper.create(order);
        // 远程调用库存,减少库存
        //storageClient.decrease(order.getProductId(),order.getCount());
        // 远程调用账户,扣减账户
        //accountClient.decrease(order.getUserId(), order.getMoney());
    }
}

5.启动 order 项目进行测试

按顺序启动服务:
  1. Eureka
  2. Seata Server
  3. Easy Id Generator
  4. Order
  5. 调用保存订单,地址:

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

在这里插入图片描述

6.测试出现异常,回滚的情况

在业务代码中加一个模拟异常再试一下:

package cn.tedu.order.service;

import cn.tedu.order.entity.Order;

import cn.tedu.order.fegin.AccountClient;
import cn.tedu.order.fegin.EasyIdClient;
import cn.tedu.order.fegin.StorageClient;
import cn.tedu.order.mapper.OrderMapper;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Random;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private EasyIdClient easyIdClient;
    @Autowired
    private AccountClient accountClient;
    @Autowired
    private StorageClient storageClient;


    @GlobalTransactional //启动全局事务,只在第一个模块上添加
    @Transactional
    @Override
    public void create(Order order) {
        // 远程调用发号器,生成订单id
        String s = easyIdClient.nextId("order_business");
        Long id = Long.valueOf(s);

        order.setId(id);
        orderMapper.create(order);

		if (Math.random() < 0.5) {
            throw new RuntimeException("模拟异常");
        }



        // 远程调用库存,减少库存
        //storageClient.decrease(order.getProductId(),order.getCount());
        // 远程调用账户,扣减账户
        //accountClient.decrease(order.getUserId(), order.getMoney());
    }
}

重启 order 项目,并调用保存订单:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100

可以看到全局事务回滚的日志:

在这里插入图片描述

订单启动全局事务部分完成,在继续之前,先把模拟异常注释掉:

        ......java

        //if (Math.random() < 0.5) {
        //    throw new RuntimeException("模拟异常");
        //}

        ......

四.storage库存服务添加 Seata AT 事务

1.配置

与订单项目中添加的配置完全相同,请参考订单配置章节配置下面三个文件:

1.application.yml
spring:
  application:
    name: storage
  datasource:
    url: jdbc:mysql:///seata_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    jdbcUrl: ${spring.datasource.url}
  cloud:
    alibaba:
      seata:
        tx-service-group: order_tx_group

# 8081  8082  8083
server:
  port: 8082
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.tedu.storage.entity
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    cn.tedu.storage.mapper: debug
2.registry.conf
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "eureka"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    cluster = "default"
  }
  eureka {
    # 连接eureka,要从注册表发现 seata-server
    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"
  }
}

3.file.conf
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



  # order_tx_group 事务组,对应使用哪个协调器
  # seata-server 是注册表中的服务id
  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
  }
}

2.创建 seata 数据源代理

与订单项目中数据源代理完全相同,请参考订单中数据源代理章节,在 cn.tedu.storage 包下创建数据源配置类 DatasourceConfiguration。

package cn.tedu.storage;

import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
public class DSPAutoConfiguration {
    // 新建原始数据源对象
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }

    // 新建数据源代理对象
    @Primary  // 首选对象
    @Bean
    public DataSource dataSourcePoxy(DataSource ds) {
        return new DataSourceProxy(ds);
    }
}

主程序注解排除 DataSourceAutoConfiguration 自动配置类。

package cn.tedu.storage;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@MapperScan("cn.tedu.storage.mapper")
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class StorageApplication {

	public static void main(String[] args) {
		SpringApplication.run(StorageApplication.class, args);
	}

}

3.启动分支事务

在业务方法上添加 @Transactional 注解启动本地事务:

package cn.tedu.storage.service;

import cn.tedu.storage.mapper.StorageMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@Service
public class StorageServiceImpl implements StorageService{
    @Autowired
    private StorageMapper storageMapper;
    @Transactional
    @Override
    public void decrease(Long productId, Integer count) {

    }
}

4.order 的业务类中调用减少商品库存

前面我们把调用商品库存注释掉了,现把注释打开:
在这里插入图片描述

5.启动 storage 项目进行测试

按顺序启动项目:

  1. Eureka
  2. Seata Server
  3. Easy Id Generator
  4. Storage
  5. Order
    调用保存订单,地址:

订单会调用库存,这两个服务会分别启动一个分支事务,两个分支事务一起组成一个全局事务:

在这里插入图片描述
观察两个项目的控制台都有Seata AT事务的日志,Storage 项目控制台如下:

在这里插入图片描述

五.account账户服务添加 Seata AT 事务

1.配置

与订单项目中添加的配置完全相同,请参考订单配置章节配置下面三个文件:

application.yml
registry.conf
file.conf

2.创建 seata 数据源代理

与订单项目中数据源代理完全相同,请参考订单中数据源代理章节,在 cn.tedu.account 包下创建数据源配置类 DatasourceConfiguration。

package cn.tedu.account;

import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
public class DSPAutoConfiguration {
    // 新建原始数据源对象
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }

    // 新建数据源代理对象
    @Primary  // 首选对象
    @Bean
    public DataSource dataSourcePoxy(DataSource ds) {
        return new DataSourceProxy(ds);
    }
}

主程序注解排除 DataSourceAutoConfiguration 自动配置类。

package cn.tedu.account;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@MapperScan("cn.tedu.account.mapper")
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class AccountApplication {

	public static void main(String[] args) {
		SpringApplication.run(AccountApplication.class, args);
	}

}

3.启动分支事务

在业务方法上添加 @Transactional 注解启动本地事务:

package cn.tedu.account.service;

import cn.tedu.account.mapper.AccountMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class AccountServiceImpl implements AccountService{
    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    @Override
    public void decrease(Long userId, BigDecimal money) {
        accountMapper.decrease(userId, money);
        if(Math.random()<0.5){
            throw new RuntimeException("模拟账户异常");

        }
    }


}

4.order 的业务类中调用扣减账户金额

前面我们把调用账户注释掉了,现把注释打开:

5.启动 account 项目进行测试

按顺序启动项目:

Eureka
Seata Server
Easy Id Generator
Storage
Account
Order

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

在这里插入图片描述

重启 account 项目,并调用保存订单:

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

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

这是 storage 的回滚日志:

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

望山。

谢谢您的打赏!!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值