Spring Cloud集成分布式事务框架Seata 1.5.2

2,076 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

背景

在前面的文章中,我们已经讲解了如何搭建Seata server1.5.2版本,接下来我们要使用搭建好的Seata server来真正集成分布式事务到我们的spring cloud项目当中。

我们在接下来的项目中,将以电商网站下单功能作为示例来讲述如何集成Seata AT模式。

image-20220930121604376.png

1.用户下单请求进入Business业务入口,也就是我们的TM,TM立马与TC请求开启全局事务,拿到XID;

2.TM拿到XID后,依次通过RPC并携带XID去调用钱包扣款创建订单

  • 在接收到远程调用时,RM获取XID,并执行业务逻辑(对应扣款、创建订单),同时生成undo_log(seata的undo_log)记录,此时分支事务还未提交,此刻RM向TC注册分支事务,并产生行锁,注册成功后,将提交分支事务;后续如果需要回滚,将根据undo_log回滚;

3.创建订单的过程中,又需要通过RPC调用实现扣减库存的操作;操作流程与第2步一样;

4.在所有服务都调用完毕正常返回后,TM发起全局事务提交动作;或者在其中任意服务调用失败后TM发起全局事务回滚动作;

5.TC收到全局事务提交或回滚请求后,依次通知所有分支事务提交或回滚,以便达到分布式事务提交或回滚的目的;

注意:在seata中,是允许TM和RM放在一个服务当中的,当前示例把TM和RM拆开来演示能够更加清晰地阐述TM和RM各自的功能,真实开发或生产环境中,开发人员可灵活使用。

项目结构

image-20220930124227180.png

1.awesome-business是整个项目的入口,该服务与前端直接交互,为前端提供服务接口;

2.awesome-account是钱包功能所在的服务,包含钱包扣款功能;

3.awesome-order包含创建订单的功能;

4.awesome-storage包含扣减库存的功能;

5.account-apiorder-apistorage-api的作用是暴露接口给awesome-business使用,做一个接口桥接的作用。

依赖

  • awesome-business作为TM,并同时对前端提供API,需要的功能包括spring mvc服务发现RPC调用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 https://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.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>awesome-business</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>awesome-business</name>
    <description>awesome-business</description>
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2021.0.4</spring-cloud.version>
    </properties>
​
    <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>
​
    <dependencies>
        <!-- spring mvc功能-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 服务发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2021.0.4.0</version>
        </dependency>
        <!-- RPC调用工具-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>3.1.4</version>
        </dependency>
        <!-- 替换默认的URLConnection,改为okhttp,并添加链接池-->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
            <version>11.9.1</version>
        </dependency>
        <!-- openFeign负载均衡由ribbon改为loadbalancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
            <version>3.1.4</version>
        </dependency>
        <!-- 解决项目启动警告:LoadBalancerCacheManager not available, returning delegate without caching.-->
        <!-- 如果注册中心有自己的缓存,那么就可以禁用loadbalancer的缓存-->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>3.1.1</version>
        </dependency>
        <!-- 解决项目启动警告:Spring Cloud LoadBalancer is currently working with the default cache. You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath.-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <!-- 分布式事务框架Seata -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <version>2021.0.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <!-- account服务暴露接口-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>account-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- order服务暴露接口-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>order-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • awesome-accountawesome-orderawesome-storage作为RM,同时向awesome-business暴露接口,需要数据库操作spring mvcseata分布式事务等功能,可能也需要RPC调用,所以对应的依赖如下:
<?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 https://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.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <!-- 记得替换artifactId -->
    <artifactId>awesome-order</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>awesome-order</name>
    <description>awesome-order</description>
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2021.0.4</spring-cloud.version>
    </properties>
​
    <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>
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- spring mvc功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--nacos注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2021.0.4.0</version>
        </dependency>
        <!-- RPC调用工具 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>3.1.4</version>
        </dependency>
        <!-- 替换默认的URLConnection,改为okhttp,并添加链接池-->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
            <version>11.9.1</version>
        </dependency>
        <!-- openFeign负载均衡由ribbon改为loadbalancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
            <version>3.1.4</version>
        </dependency>
        <!-- 解决项目启动警告:LoadBalancerCacheManager not available, returning delegate without caching.-->
        <!-- 如果注册中心有自己的缓存,那么就可以禁用loadbalancer的缓存-->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>3.1.1</version>
        </dependency>
        <!-- 解决项目启动警告:Spring Cloud LoadBalancer is currently working with the default cache. You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath.-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <!--seata分布式事务框架-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <version>2021.0.4.0</version>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--  【数据库】mysql driver  -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <!--【数据库】数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.11</version>
        </dependency>
        <!--【数据库】持久层框架 mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!--【数据库】分页插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.3</version>
        </dependency>
        <!-- order服务自己的接口 -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>order-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- storage服务暴露的接口 -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>storage-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
​
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.yml</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <!--     mybatis-generator maven插件,用于生成DAO代码       -->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.1</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                    <!-- mybatis-generator配置文件路径 -->
                    <configurationFile>${basedir}/src/main/resources/mybatis/generator/generatorConfig.xml
                    </configurationFile>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

以上是awesome-order服务对应的依赖,awesome-accountawesome-storage可参照上面的依赖按需修改,awesome-accountawesome-storage因为不需要RPC调用,可删除对应的RPC调用依赖及其相关的优化关联的依赖,也就是以下这一段:

<!-- RPC调用工具 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>3.1.4</version>
        </dependency>
        <!-- 替换默认的URLConnection,改为okhttp,并添加链接池-->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
            <version>11.9.1</version>
        </dependency>
        <!-- openFeign负载均衡由ribbon改为loadbalancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
            <version>3.1.4</version>
        </dependency>
        <!-- 解决项目启动警告:LoadBalancerCacheManager not available, returning delegate without caching.-->
        <!-- 如果注册中心有自己的缓存,那么就可以禁用loadbalancer的缓存-->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>3.1.1</version>
        </dependency>
        <!-- 解决项目启动警告:Spring Cloud LoadBalancer is currently working with the default cache. You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath.-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
  • account-apiorder-apistorage-api作为桥接层,只需要spring mvc的功能即可,所以它们对应的依赖为:
<?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 https://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.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <!-- 记得修改对应的artifactId -->
    <artifactId>account-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>account-api</name>
    <description>account-api</description>
​
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
​
    <dependencies>
        <!-- spring mvc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

至此,所有服务对应的依赖就全部搞定。

数据表的建立

awesome-business是不需要操作数据库的,所以不存在数据表,只有awesome-accountawesome-orderawesome-storage三个RM服务需要操作数据库,所以需要建立数据表。

  • awesome-account需要创建钱包对应的表,另外就是seata的undo_log
DROP TABLE IF EXISTS `wallet_tbl`;
CREATE TABLE `wallet_tbl` (
  `id` int NOT NULL COMMENT '主键',
  `user_id` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '用户ID',
  `money` int NOT NULL COMMENT '账户余额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- 初始化钱包数据
-- ----------------------------
INSERT INTO `wallet_tbl` (`id`, `user_id`, `money`) VALUES (1, '123456', 100000);
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
  `branch_id` bigint NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(128) NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
  `rollback_info` longblob NOT NULL COMMENT 'rollback info',
  `log_status` int 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 KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AT transaction mode undo table';
  • awesome-order需要创建订单表与undo_log
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户ID',
  `commodity_code` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '商品编码',
  `count` int NOT NULL COMMENT '数量',
  `unit_price` int NOT NULL COMMENT '单价',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
  `branch_id` bigint NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(128) NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
  `rollback_info` longblob NOT NULL COMMENT 'rollback info',
  `log_status` int 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 KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AT transaction mode undo table';
  • awesome-storage需要创建库存表与undo_log
DROP TABLE IF EXISTS `stock_tbl`;
CREATE TABLE `stock_tbl` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `commodity_code` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '商品编码',
  `count` int NOT NULL COMMENT '库存数',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- 初始化库存数据
-- ----------------------------
INSERT INTO `stock_tbl` (`id`, `commodity_code`, `count`) VALUES (1, 'CC-54321', 100);
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
  `branch_id` bigint NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(128) NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
  `rollback_info` longblob NOT NULL COMMENT 'rollback info',
  `log_status` int 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 KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AT transaction mode undo table';

至此,所有服务的数据表已经建立完毕。

服务配置

接下来,我们需要根据需要给每个服务加上对应的配置。

  • awesome-business作为业务入口服务,需要配置nacos服务发现、openFeign远程RPC调用工具、seata分布式框架:
spring:
  cloud:
    nacos:
      discovery:
        enabled: true
        server-addr: "host:端口"
        # 需要在nacos中创建awesome-seata命名空间
        namespace: "awesome-seata"
        group: ${spring.profiles.active}
        username: "用户名"
        password: "密码"
    # openFeign优化,是否开启缓存,如果服务发现已经有缓存了,就可以不用开启缓存
    loadbalancer:
      cache:
        enabled: true
        # 过期时间10s
        ttl: 10
        # 容量256M
        capacity: 256
        caffeine:
          #          initialCapacity=[integer]: sets Caffeine.initialCapacity.
          #          maximumSize=[long]: sets Caffeine.maximumSize.
          #          maximumWeight=[long]: sets Caffeine.maximumWeight.
          #          expireAfterAccess=[duration]: sets Caffeine.expireAfterAccess(long, java.util.concurrent.TimeUnit).
          #          expireAfterWrite=[duration]: sets Caffeine.expireAfterWrite(long, java.util.concurrent.TimeUnit).
          #          refreshAfterWrite=[duration]: sets Caffeine.refreshAfterWrite(long, java.util.concurrent.TimeUnit).
          #          weakKeys: sets Caffeine.weakKeys().
          #          weakValues: sets Caffeine.weakValues().
          #          softValues: sets Caffeine.softValues().
          #          recordStats: sets Caffeine.recordStats().
          #         initialCapacity初始化键值对的数量
          spec: initialCapacity=500,expireAfterWrite=5s
# openFeign优化
feign:
  # 开启压缩功能
  compression:
    request:
      enabled: true
      mime-types: text/xml,application/xml,application/json
      min-request-size: 2048
    response:
      enabled: true
  # 不使用httpclient,改用okhttp
  httpclient:
    enabled: false
  okhttp:
    enabled: true
    # 是否禁用重定向
    follow-redirects: true
    connect-timeout: 5000
    # 链接失败是否重试
    retry-on-connection-failure: true
    read-timeout: 5000
    write-timeout: 5000
    # 最大空闲数量
    max-idle-connections: 5
    # 生存时间
    keep-alive-duration: 15000
  client:
    config:
      # 设置超时,囊括了okhttp的超时,okhttp属于真正执行的超时,openFeign属于服务间的超时
      # 设置全局超时时间
      default:
        connectTimeout: 2000
        readTimeout: 5000
      #   针对contextId设置超时时间
      walletApi:
        connectTimeout: 1000
        readTimeout: 2000
#        loggerLevel: FULL
seata:
  #  开启seata
  enabled: true
  #  注册中心找TC服务
  registry:
    type: nacos
    nacos:
      cluster: "default"
      username: "用户名"
      password: "密码"
      server-addr: "host:端口"
      group: SEATA_GROUP
      # 需要在nacos中创建awesome-seata命名空间
      namespace: seata-server
      application: seata-server
  application-id: ${spring.application.name}
  #  事务分组,需要对应vgroup-mapping下面的key
  tx-service-group: shanghai
  service:
    vgroup-mapping:
      # 该分组对应的TC集群名称
      # default对应TC的cluster字段
      shanghai: default
# 日志配置
logging:
  pattern:
    # 日志输出格式
    console: "%d{yyyy-MM-dd HH:mm:ss} %clr(%5p) [%thread] %clr(%logger){cyan} : %msg%n"
  level:
    #    trace < debug < info < warn < error < fatal
    # 全局日志级别
    root: info
    # 指定包日志级别
    com.example.awesomeabusiness: warn

该配置中OpenFeign优化思路可以参考博客:openFeign的集成与优化

  • awesome-accountawesome-orderawesome-storage三个服务需要操作数据库、以及相关的分布式事务服务,所以对应的配置如下:
spring:
  # 数据库链接配置
  datasource:
    url: jdbc:mysql://host:端口/awesome-order?useUnicode=true&characterEncoding=UTF8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: "用户名"
    password: "密码"
    # 链接池
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      # 链接池初始化大小
      initial-size: 8
      # 最大活跃数
      max-active: 16
      # 最小空闲数
      min-idle: 1
      # 最大等待时间
      max-wait: 60000
  # 微服务配置
  cloud:
    nacos:
      discovery:
        enabled: true
        server-addr: "host:端口"
        namespace: "awesome-seata"
        group: ${spring.profiles.active}
        username: nacos
        password: nacos
        service: ${spring.application.name}
        register-enabled: true
        # 配置注册的目标ip,根据自己的需求设置
        ip: "127.0.0.1"
        # 注册的目标端口
        port: ${server.port}
        metadata:
          environment: ${spring.profiles.active}
          version: ${project.version}
    # openFeign优化
    loadbalancer:
      cache:
        enabled: true
        # 过期时间10s
        ttl: 10
        # 容量256M
        capacity: 256
        caffeine:
          #          initialCapacity=[integer]: sets Caffeine.initialCapacity.
          #          maximumSize=[long]: sets Caffeine.maximumSize.
          #          maximumWeight=[long]: sets Caffeine.maximumWeight.
          #          expireAfterAccess=[duration]: sets Caffeine.expireAfterAccess(long, java.util.concurrent.TimeUnit).
          #          expireAfterWrite=[duration]: sets Caffeine.expireAfterWrite(long, java.util.concurrent.TimeUnit).
          #          refreshAfterWrite=[duration]: sets Caffeine.refreshAfterWrite(long, java.util.concurrent.TimeUnit).
          #          weakKeys: sets Caffeine.weakKeys().
          #          weakValues: sets Caffeine.weakValues().
          #          softValues: sets Caffeine.softValues().
          #          recordStats: sets Caffeine.recordStats().
          #         initialCapacity初始化键值对的数量
          spec: initialCapacity=500,expireAfterWrite=5s
#openFeign优化
feign:
  # 开启压缩功能
  compression:
    request:
      enabled: true
      mime-types: text/xml,application/xml,application/json
      min-request-size: 2048
    response:
      enabled: true
  # 不使用httpclient,改用okhttp
  httpclient:
    enabled: false
  okhttp:
    enabled: true
    # 是否禁用重定向
    follow-redirects: true
    connect-timeout: 5000
    # 链接失败是否重试
    retry-on-connection-failure: true
    read-timeout: 5000
    write-timeout: 5000
    # 最大空闲数量
    max-idle-connections: 5
    # 生存时间
    keep-alive-duration: 15000
  client:
    config:
      # 设置超时,囊括了okhttp的超时,okhttp属于真正执行的超时,openFeign属于服务间的超时
      # 设置全局超时时间
      default:
        connectTimeout: 2000
        readTimeout: 5000

# mybatis配置
mybatis:
  check-config-location: true
  #  mybatis框架配置文件,对mybatis的生命周期起作用
  config-location: "classpath:mybatis/mybatis-config.xml"
  #  配置xml路径
  mapper-locations: "classpath:mybatis/mapper/*Mapper.xml"
  #  配置model包路径
  type-aliases-package: "com.example.awesomeorder.dao.entity.*"

# 分布式事务配置
seata:
  #  开启seata
  enabled: true
  #  注册中心找TC服务
  registry:
    type: nacos
    nacos:
      cluster: "default"
      username: "nacos"
      password: "nacos"
      server-addr: "host:端口"
      group: SEATA_GROUP
      namespace: seata-server
      application: seata-server
  application-id: ${spring.application.name}
  #  事务分组
  tx-service-group: shanghai
  service:
    vgroup-mapping:
      # 该分组对应的TC集群名称
      shanghai: default

# 日志配置
logging:
  pattern:
    # 日志输出格式
    console: "%d{yyyy-MM-dd HH:mm:ss} %clr(%5p) [%thread] %clr(%logger){cyan} : %msg%n"
  level:
    #    trace < debug < info < warn < error < fatal
    # 全局日志级别
    root: info
    # 指定包日志级别
    com.example.awesomeorder: warn

上述配置属于awesome-orderawesome-accountawesome-storage没有rpc调用的可以删掉OpenFeign相关的配置:

spring:
  # 数据库链接配置
  datasource:
    url: jdbc:mysql://host:端口/数据库名称?useUnicode=true&characterEncoding=UTF8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: "用户名"
    password: "密码"
    # 链接池
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      # 链接池初始化大小
      initial-size: 8
      # 最大活跃数
      max-active: 16
      # 最小空闲数
      min-idle: 1
      # 最大等待时间
      max-wait: 60000
  # 微服务配置
  cloud:
    nacos:
      discovery:
        enabled: true
        server-addr: "host:端口"
        namespace: "awesome-seata"
        group: ${spring.profiles.active}
        username: nacos
        password: nacos
        service: ${spring.application.name}
        register-enabled: true
        # 配置注册的目标ip,根据自己的需求设置
        ip: "127.0.0.1"
        # 注册的目标端口
        port: ${server.port}
        metadata:
          environment: ${spring.profiles.active}
          version: ${project.version}
# mybatis配置
mybatis:
  check-config-location: true
  #  mybatis框架配置文件,对mybatis的生命周期起作用
  config-location: "classpath:mybatis/mybatis-config.xml"
  #  配置xml路径
  mapper-locations: "classpath:mybatis/mapper/*Mapper.xml"
  #  配置model包路径,根据包路径修改
  type-aliases-package: "com.example.awesomeaccount.dao.entity.*"

# 分布式事务配置
seata:
  #  开启seata
  enabled: true
  #  注册中心找TC服务
  registry:
    type: nacos
    nacos:
      cluster: "default"
      username: "nacos"
      password: "nacos"
      server-addr: "host:端口"
      group: SEATA_GROUP
      namespace: seata-server
      application: seata-server
  application-id: ${spring.application.name}
  #  事务分组
  tx-service-group: shanghai
  service:
    vgroup-mapping:
      # 该分组对应的TC集群名称
      shanghai: default

# 日志配置
logging:
  pattern:
    # 日志输出格式
    console: "%d{yyyy-MM-dd HH:mm:ss} %clr(%5p) [%thread] %clr(%logger){cyan} : %msg%n"
  level:
    #    trace < debug < info < warn < error < fatal
    # 全局日志级别
    root: info
    # 指定包日志级别,根据包路径修改
    com.example.awesomeaccount: warn

代码实现

  • 首先我们主要看一下awesome-business项目中的service实现:
package com.example.awesomebusiness.service.impl;
​
import com.example.accountapi.model.AmountInfo;
import com.example.awesomebusiness.api.OrderApiClient;
import com.example.awesomebusiness.api.WalletApiClient;
import com.example.awesomebusiness.service.ShoppingCartService;
import com.example.orderapi.model.OrderInfo;
import io.seata.core.exception.GlobalTransactionException;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
​
import javax.annotation.Resource;
​
/**
 * @author zouwei
 * @className ShoppingCartServiceImpl
 * @date: 2022/9/18 14:01
 * @description:
 */
@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {
  // 钱包服务
  @Resource
  private WalletApiClient walletApiClient;
  // 订单服务
  @Resource
  private OrderApiClient orderApiClient;
​
  // 别忘记了这个注解,这是开启分布式事务的标记,前提是当前业务逻辑不处于任何的分布式事务当中才能开启新的分布式事务
  @GlobalTransactional
  public String placeOrder() throws GlobalTransactionException {
    // 模拟用户ID 123456,对应数据库初始化的用户ID
    String userId = "123456";
    // 构建订单数据
    OrderInfo orderInfo = new OrderInfo();
    // 数量15个
    orderInfo.setCount(15);
    // 商品编码,对应库存数据表的初始化数据
    orderInfo.setCommodityCode("CC-54321");
    // 单价299,默认是long类型,单位分;避免double精度丢失
    orderInfo.setUnitPrice(299);
    // 订单归属
    orderInfo.setUserId(userId);
    // 计算扣款金额,数量*单价
    long amount = orderInfo.getCount() * orderInfo.getUnitPrice();
    // 构建扣款数据
    AmountInfo amountInfo = new AmountInfo();
    // 设置扣款金额
    amountInfo.setAmount(amount);
    // 设置扣款主体
    amountInfo.setUserId(userId);
    // 先扣款,扣款成功就创建订单,扣减库存在创建订单的逻辑里面
    if (walletApiClient.deductMoney(amountInfo) && orderApiClient.createOrder(orderInfo)) {
      return "下单成功!";
    }
    // 1.扣款失败,抛异常,分布式事务回滚
    // 2.创建订单失败,抛异常,分布式事务回滚
    throw new GlobalTransactionException("下单失败!");
  }
}
​
package com.example.awesomebusiness.api;
​
import com.example.accountapi.api.WalletApi;
import org.springframework.cloud.openfeign.FeignClient;
​
/**
 * account:注册的服务名称;
 * contextId:注册到spring中的Bean名称,保证唯一性
 * path:对应服务端的requestMapping里面的value值,没有的话,可不写
 *
 * @author zouwei
 * @className AccountApiClient
 * @date: 2022/9/18 14:42
 * @description:
 */
@FeignClient(name = "account", contextId = "walletApi", path = "/wallet")
public interface WalletApiClient extends WalletApi {
}
​
package com.example.awesomebusiness.api;
​
import com.example.orderapi.api.OrderApi;
import org.springframework.cloud.openfeign.FeignClient;
​
/**
 * @author zouwei
 * @className OrderApiClient
 * @date: 2022/9/27 23:24
 * @description:
 */
@FeignClient(name = "order", contextId = "orderApi", path = "/order")
public interface OrderApiClient extends OrderApi {
}
  • 接下来看看awesome-account的业务逻辑:
package com.example.awesomeaccount.service.impl;
​
import com.example.awesomeaccount.dao.mapper.WalletEnhanceMapper;
import com.example.awesomeaccount.service.IWalletService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
​
import javax.annotation.Resource;
​
/**
 * @author zouwei
 * @className WalletServiceImpl
 * @date: 2022/9/27 21:20
 * @description:
 */
@Service
public class WalletServiceImpl implements IWalletService {
​
  @Resource
  private WalletEnhanceMapper walletEnhanceMapper;
​
  @Transactional
  @Override
  public Boolean deductMoney(String userId, long amount) {
    // 判断修改后的影响行数是否大于0
    
    // 对应sql:update wallet_tbl set money = money - #{amount,jdbcType=INTEGER} where user_id = #{userId,jdbcType=VARCHAR} and money <![CDATA[ >= ]]>#{amount,jdbcType=INTEGER}
    
    // WalletEnhanceMapper属于自定义的sql mapper,没有写在WalletMapper中,是因为避免重新生成Mapper的时候,自定义sql被覆盖
    return walletEnhanceMapper.deductMoney(userId, amount) > 0;
  }
}

对应的controller如下:

package com.example.awesomeaccount.controller;
​
import com.example.accountapi.api.WalletApi;
import com.example.accountapi.model.AmountInfo;
import com.example.awesomeaccount.service.IWalletService;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * WalletApi接口在account-api模块中
 *
 * @author zouwei
 * @className WalletController
 * @date: 2022/9/18 14:58
 * @description:
 */
@Slf4j
@RestController
@RequestMapping("/wallet")
public class WalletController implements WalletApi {
​
  @Autowired
  private IWalletService walletService;
​
  @Override
  public Boolean deductMoney(AmountInfo amountInfo) {
    String userId = amountInfo.getUserId();
    long amount = amountInfo.getAmount();
    // 打印分布式事务XID
    log.warn("XID:{}", RootContext.getXID());
    // 扣款
    return walletService.deductMoney(userId, amount);
  }
}
package com.example.accountapi.api;
​
import com.example.accountapi.model.AmountInfo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
​
/**
 * 供生产者和消费者共同使用
 *
 * @author zouwei
 * @className AccountApi
 * @date: 2022/9/18 14:31
 * @description:
 */
public interface WalletApi {
​
  /**
   * 扣钱
   *
   * @param amountInfo
   * @return
   */
  @PostMapping("/deductMoney")
  Boolean deductMoney(@RequestBody AmountInfo amountInfo);
}
  • awesome-orderawesome-storage服务也是类似的代码结构:
package com.example.awesomeorder.service.impl;
​
import com.example.awesomeorder.api.StockApiClient;
import com.example.awesomeorder.dao.entity.Order;
import com.example.awesomeorder.dao.mapper.OrderMapper;
import com.example.awesomeorder.service.IOrderService;
import com.example.storageapi.model.OrderInfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
​
import javax.annotation.Resource;
import java.time.LocalDateTime;
​
/**
 * @author zouwei
 * @className OrderServiceImpl
 * @date: 2022/9/27 22:47
 * @description:
 */
@Service
public class OrderServiceImpl implements IOrderService {
​
  @Resource
  private OrderMapper orderMapper;
  
  @Resource
  private StockApiClient stockApiClient;
​
  /**
   * 创建订单
   *
   * @param userId
   * @param commodityCode
   * @param count
   * @param unitPrice
   * @return
   */
  @Transactional
  @Override
  public Boolean createOrder(String userId, String commodityCode, int count, long unitPrice) {
    // 构建待扣减的库存信息
    OrderInfo orderInfo = new OrderInfo();
    // 设置商品编码
    orderInfo.setCommodityCode(commodityCode);
    // 设置需要扣减的数量
    orderInfo.setCount(count);
    // 先构建库存
    if (stockApiClient.deductStock(orderInfo)) {
      // 扣减库存成功后,准备创建订单
      Order order = new Order();
      // 创建时间
      order.setCreateTime(LocalDateTime.now());
      // 用户ID
      order.setUserId(userId);
      // 数量
      order.setCount(count);
      // 商品编码
      order.setCommodityCode(commodityCode);
      // 单价
      order.setUnitPrice(unitPrice);
      // 创建订单
      return orderMapper.insert(order) > 0;
    }
    // 扣减库存失败,订单创建失败
    return Boolean.FALSE;
  }
}
package com.example.awesomeorder.api;
​
import com.example.storageapi.api.StockApi;
import org.springframework.cloud.openfeign.FeignClient;
​
/**
 * @author zouwei
 * @className StockApiClient
 * @date: 2022/9/28 00:34
 * @description:
 */
@FeignClient(name = "storage", contextId = "stockApi", path = "/stock")
public interface StockApiClient extends StockApi {
}
package com.example.awesomeorder.controller;
​
import com.example.awesomeorder.service.IOrderService;
import com.example.orderapi.api.OrderApi;
import com.example.orderapi.model.OrderInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * @author zouwei
 * @className OrderController
 * @date: 2022/9/27 22:55
 * @description:
 */
@RestController
@RequestMapping("/order")
public class OrderController implements OrderApi {
​
  @Autowired
  private IOrderService orderService;
​
  @Override
  public Boolean createOrder(OrderInfo orderInfo) {
    String commodityCode = orderInfo.getCommodityCode();
    int count = orderInfo.getCount();
    long unitPrice = orderInfo.getUnitPrice();
    String userId = orderInfo.getUserId();
    return orderService.createOrder(userId, commodityCode, count, unitPrice);
  }
}
package com.example.orderapi.api;
​
import com.example.orderapi.model.OrderInfo;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
​
/**
 * @author zouwei
 * @className OrderApi
 * @date: 2022/9/27 22:57
 * @description:
 */
public interface OrderApi {
​
  @PutMapping("/create")
  Boolean createOrder(@RequestBody OrderInfo orderInfo);
}
​
  • 接下来,就剩下awesome-storage服务了:
package com.example.awesomestorage.service.impl;
​
import com.example.awesomestorage.dao.mapper.StockEnhanceMapper;
import com.example.awesomestorage.service.IStockService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
​
import javax.annotation.Resource;
​
/**
 * @author zouwei
 * @className StockServiceImpl
 * @date: 2022/9/28 00:06
 * @description:
 */
@Service
public class StockServiceImpl implements IStockService {
​
  @Resource
  private StockEnhanceMapper stockEnhanceMapper;
​
  @Transactional
  @Override
  public Boolean deductStock(String commodityCode, int count) {
    // 扣减库存,判断影响行数是否大于0
    // sql如下: update stock_tbl set count = count - #{count,jdbcType=INTEGER} where commodity_code = #{commodityCode,jdbcType=VARCHAR} and count <![CDATA[ >= ]]> #{count,jdbcType=INTEGER}
    return stockEnhanceMapper.deductStock(commodityCode, count) > 0;
  }
}
package com.example.awesomestorage.controller;
​
import com.example.awesomestorage.service.IStockService;
import com.example.storageapi.api.StockApi;
import com.example.storageapi.model.OrderInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * @author zouwei
 * @className StockController
 * @date: 2022/9/28 00:23
 * @description:
 */
@RestController
@RequestMapping("/stock")
public class StockController implements StockApi {
​
  @Autowired
  private IStockService stockService;
​
  @Override
  public Boolean deductStock(OrderInfo orderInfo) {
    String commodityCode = orderInfo.getCommodityCode();
    int count = orderInfo.getCount();
    return stockService.deductStock(commodityCode, count);
  }
}
package com.example.storageapi.api;
​
import com.example.storageapi.model.OrderInfo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
​
/**
 * @author zouwei
 * @className StockApi
 * @date: 2022/9/28 00:26
 * @description:
 */
public interface StockApi {
​
  @PostMapping("/deduct")
  Boolean deductStock(@RequestBody OrderInfo orderInfo);
}
​

至此,这几个服务的核心代码就全部搞定,我们千万别忘记了在项目入口类中加上对应的注解,比如awesome-business入口类:

package com.example.awesomebusiness;
​
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
​
// 开启nacos,不过仅仅使用服务发现,不需要注册
@EnableDiscoveryClient(autoRegister = false)
// 开启OpenFeign
@EnableFeignClients
@SpringBootApplication
public class AwesomeBusinessApplication {
​
  public static void main(String[] args) {
    SpringApplication.run(AwesomeBusinessApplication.class, args);
  }
}

awesome-order的入口类有些许不同:

package com.example.awesomeorder;
​
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
​
// 增加mybatis的MapperScan注解
@MapperScan(basePackages = "com.example.awesomeorder.dao.mapper")
// RPC调用才需要OpenFeign
@EnableFeignClients
// 既要注册当前服务,也要发现其他服务,比如storage库存服务
@EnableDiscoveryClient
@SpringBootApplication
public class AwesomeOrderApplication {
​
  public static void main(String[] args) {
    SpringApplication.run(AwesomeOrderApplication.class, args);
  }
}

如果小伙伴们对于mybatis的配置有疑虑的话,可以查看博客:手把手教你springboot集成mybatis;另外我怕也会将当前项目代码上传到github,想了解的小伙伴可以自己下载:awesome-seata

小结

通过以上示例,我们需要了解到以下几点,才能帮助我们更好地继承seata AT模式到我们的项目当中:

1.TM只负责全局事务的管理,比如全局事务的开启、提交或回滚;

2.TM可以和RM共存,也就是说,一个服务中,可以既有TM,也有RM;

3.RM只负责分支事务的管理,比如分支事务的注册、上报、提交或回滚;

4.多个分支事务是否在同一个分布式事务当中,是通过XID来判断的,XID是通过RPC来传递的;

5.TC相当于一个传话筒和记事本,负责记录和维护全局事务和分支事务的状态,以及下发TM提交或回滚的指令给到RM;

6.在RM当中是不需要使用@GlobalTransactional注解的,只需要使用@Transactional注解来保证分支事务的原子性;

7.所有的RM服务都需要有undo_log数据表,这样才能使用AT模式;