跳至主要內容

Seata

sixkey大约 17 分钟后端SpringCloud微服务Seata

Seata

CloudAlibaba之Seata

问题引出

一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题,但是,关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题。

单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

31ea4fb4c944da18970a3a1e68357c9d.png

结论

迫切希望提供一种分布式事务框架,解决微服务架构下的分布式事务问题

简介

是什么?

Simple Extensible Autonomous Transaction ArchitectureSeata

Apache Seata(incubating) 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

能干嘛?

解决分布式事务

去哪下?

官网:Apache Seataopen in new window

怎么玩?

  • 本地@Transactional

  • 全局@GlobalTransactional

  • Seata的分布式交易解决方案

    3c515c833d82540654e36ad43b6aeb93.png
    3c515c833d82540654e36ad43b6aeb93.png

工作流程简介

1、纵观整个分布式事务的管理,就是全局事务ID的传递和变更,要让开发者无感知。

cc22fad2ef0170bb12ee3632c0f9d8eb.png

2、seata对分布式事务的协调和控制就是1+3

  • 一个XID

    XID是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。

  • 官网版3个概念(TC—>TM—>RM)

    b6af9b0c42eafd4200fd3a34f72ff50b.png
  • 个别版3个概念

    a3083a4ad75b6619a2eb2b0f70241b1f.png
    • TC (Transaction Coordinator) - 事务协调器
      • 就是seata,负责维护全局事务和分支事务的状态。驱动全局事务提交或回滚。
    • TM (Transaction Manager) - 事务管理器
      • 标注全局@GlobalTransactional启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据TC维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
    • RM (Resource Manager) - 资源管理器
      • 就是mysql数据库本身,可以是多个RM,负责管理分支事务上的资源,向TC注册分支事,汇报分支事务状态,驱动分支事务的提交或回滚。

3、分布式事务的执行流程———小总结

三个组件相互协作,TC以Seata 服务器(Server)形式独立部署,TM和RM则是以Seata Client的形式集成在微服务中运行,

流程如下:

172b5796b545bb86f24e06330f317cda.png

4、各事务模式

64d08732e0f4fd6beef94760b48cb8bb.png
64d08732e0f4fd6beef94760b48cb8bb.png

我们以AT模式进行学习

Seata-Server2.0.0安装

下载地址

Seata-Server下载 | Apache Seataopen in new window

配置参数参考

https://seata.apache.org/zh-cn/docs/user/configuration

新手部署指南

d6f2e7b83658276ff3935e02f72b491f.png

步骤

建库

1、新建数据库Seata

CREATE DATABASE seata;
USE seata;

SQL建表地址

https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql

SQL脚本

CREATE TABLE IF NOT EXISTS `global_table`

(

    `xid`                       VARCHAR(128) NOT NULL,

    `transaction_id`            BIGINT,

    `status`                    TINYINT      NOT NULL,

    `application_id`            VARCHAR(32),

    `transaction_service_group` VARCHAR(32),

    `transaction_name`          VARCHAR(128),

    `timeout`                   INT,

    `begin_time`                BIGINT,

    `application_data`          VARCHAR(2000),

    `gmt_create`                DATETIME,

    `gmt_modified`              DATETIME,

    PRIMARY KEY (`xid`),

    KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),

    KEY `idx_transaction_id` (`transaction_id`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



-- the table to store BranchSession data

CREATE TABLE IF NOT EXISTS `branch_table`

(

    `branch_id`         BIGINT       NOT NULL,

    `xid`               VARCHAR(128) NOT NULL,

    `transaction_id`    BIGINT,

    `resource_group_id` VARCHAR(32),

    `resource_id`       VARCHAR(256),

    `branch_type`       VARCHAR(8),

    `status`            TINYINT,

    `client_id`         VARCHAR(64),

    `application_data`  VARCHAR(2000),

    `gmt_create`        DATETIME(6),

    `gmt_modified`      DATETIME(6),

    PRIMARY KEY (`branch_id`),

    KEY `idx_xid` (`xid`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



-- the table to store lock data

CREATE TABLE IF NOT EXISTS `lock_table`

(

    `row_key`        VARCHAR(128) NOT NULL,

    `xid`            VARCHAR(128),

    `transaction_id` BIGINT,

    `branch_id`      BIGINT       NOT NULL,

    `resource_id`    VARCHAR(256),

    `table_name`     VARCHAR(32),

    `pk`             VARCHAR(36),

    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',

    `gmt_create`     DATETIME,

    `gmt_modified`   DATETIME,

    PRIMARY KEY (`row_key`),

    KEY `idx_status` (`status`),

    KEY `idx_branch_id` (`branch_id`),

    KEY `idx_xid` (`xid`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



CREATE TABLE IF NOT EXISTS `distributed_lock`

(

    `lock_key`       CHAR(20) NOT NULL,

    `lock_value`     VARCHAR(20) NOT NULL,

    `expire`         BIGINT,

    primary key (`lock_key`)

) ENGINE = InnoDB

  DEFAULT CHARSET = utf8mb4;



INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

结果

c3d334d616178e4336c0c96e98bbfb52.png
c3d334d616178e4336c0c96e98bbfb52.png
改配置

修改seata-server-2.O.O\conf\appIication.yml配置,记得先备份

e3c44b72ab2eb2a0c42767f5a39ab4be.png

application.yml定稿版(一般使用里面的配置足矣)

#  Copyright 1999-2019 Seata.io Group.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

 
# Seata控制台访问端口
server:
  port: 7091

 
spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${log.home:${user.home}/logs/seata}
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

 
# Seata控制台登录用户密码
console:
  user:
    username: seata
    password: seata

seata:
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace:
      group: SEATA_GROUP         #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
      username: nacos
      password: nacos
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
      namespace:
      cluster: default
      username: nacos
      password: nacos    

  store:
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
      user: root
      password: '020708'
      min-conn: 10
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      query-limit: 1000
      max-wait: 5000

  #  server:
  #    service-port: 8091 #If not configured, the default is '${server.port} + 1000'

  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
启动

先启动Nacos

再启动seata-server.bat

访问http://127.0.0.1:7091

090b7123c1de0ae629aea21b759d9e03.png
090b7123c1de0ae629aea21b759d9e03.png

再看Nacos,可以看到Seata已经注册到Nacos中了

0b58cda43153aa2369202d48935aed75.png
0b58cda43153aa2369202d48935aed75.png

案例实战

案例实战说明:

即将准备三个微服务模块:订单模块、库存模块、账户余额模块

主要说明演示:使用分布式事务Seata前后的区别说明:不使用分布式事务有什么影响,使用分布式事务可以解决哪些痛点。

数据库和表准备

注意:以下演示Nacos和Seata都要先保证启动ok

案例业务说明:

这里我们创建三个服务,一个订单服务,一个库存服务,一个账户服务。-------最下面还有笔记

当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,

再通过远程调用账户服务来扣减用户账户里面的余额,

最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

下订单 → 减库存 → 扣余额 → 改(订单)状态

731a49082a79eb33e9a81d9a802f5602.png
建库

针对3个微服务建各自的数据库

5368036f58dd62223d28e3726312990b.png
5368036f58dd62223d28e3726312990b.png

SQL脚本

CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
建表

针对这3个库,各自需要建对应的undo_log回滚日志表

SQL脚本

CREATE TABLE IF NOT EXISTS `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(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 KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

结果

777c146a62862ee5d3b8c2290dd03fe6.png

针对这3个库,各自需要建对应的业务表

seata_order库建t_order表

CREATE TABLE t_order(

`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,

`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',

`product_id` BIGINT(11)DEFAULT NULL COMMENT '产品id',

`count` INT(11) DEFAULT NULL COMMENT '数量',

`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',

`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'

)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

SELECT * FROM t_order;

seata_account建t_account

CREATE TABLE t_account(

`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',

`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',

`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',

`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用账户余额',

`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'

)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

 

INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');

SELECT * FROM t_account;

seata_storage建t_storage

CREATE TABLE t_storage(

`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,

`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',

`total` INT(11) DEFAULT NULL COMMENT '总库存',

`used` INT(11) DEFAULT NULL COMMENT '已用库存',

`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'

)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

 

INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');


SELECT * FROM t_storage;
最终效果
6b4d2888dbb8fc9913ca09cd7eb1bb02.png

编码落地实现

业务需求:下订单——>减库存——>扣余额——>改订单状态

创建openfeign需要远程调用的一些接口方法

seata-account-service远程接口

/**
 * @Author: @weixueshi
 * @Create: 2024/4/15 - 13:07
 * @Version: v1.0
 */
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi
{
    /**
     * 扣减账户余额
     */
    @PostMapping("/account/decrease")
    ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}

seata-storage-service远程接口

/**
 * @Author: @weixueshi
 * @Create: 2024/4/15 - 13:07
 * @Version: v1.0
 */
@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi
{
    /**
     * 扣减库存
     */
    @PostMapping(value = "/storage/decrease")
    ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

新建订单Order微服务
  • 建Model

    • seata-order-service2001
  • 改pom

    <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
    
        <dependencies>
            <!--自定义-->
            <dependency>
                <groupId>org.cloud</groupId>
                <artifactId>cloud-commons</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--自定义openfeign服务-->
            <dependency>
                <groupId>org.cloud</groupId>
                <artifactId>cloudAlibaba-openfeign-consumer</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--alibaba-seata-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            </dependency>
            <!--openfeign-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <!--loadbalancer-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
            <!--nacos-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!--SpringBoot通用依赖模块-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <!--SpringBoot集成druid连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
            </dependency>
            <!--mybatis-plus和springboot整合-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            </dependency>
            <!--Mysql数据库驱动8 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <!--persistence-->
            <dependency>
                <groupId>javax.persistence</groupId>
                <artifactId>persistence-api</artifactId>
            </dependency>
            <!--hutool-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
            </dependency>
            <!-- fastjson2 -->
            <dependency>
                <groupId>com.alibaba.fastjson2</groupId>
                <artifactId>fastjson2</artifactId>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.28</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
  • 写yml

    server:
      port: 2001
    
    spring:
      application:
        name: seata-order-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848         #Nacos服务注册中心地址
      # ==========applicationName + druid-mysql8 driver===================
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
        username: root
        password: '020708'
    # ========================mybatis===================
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      type-aliases-package: org.cloud.entity
      configuration:
        map-underscore-to-camel-case: true
    
    # ========================seata===================
    seata:
      registry:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          namespace: ""
          group: SEATA_GROUP
          application: seata-server
      tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
      service:
        vgroup-mapping: # 点击源码分析
          default_tx_group: default # 事务组与TC服务集群的映射关系
      data-source-proxy-mode: AT
    
    logging:
      level:
        io:
          seata: info
    
  • 启动类

    @SpringBootApplication
    @MapperScan("org.cloud.mapper")
    @EnableDiscoveryClient //服务注册和发现
    @EnableFeignClients
    public class SeataOrderServiceApp
    {
        public static void main( String[] args )
        {
            SpringApplication.run(SeataOrderServiceApp.class,args);
        }
    }
    
  • 业务类

    这里省略了mapper等哪些创建

    OrderController

@RestController
@RequestMapping("/order")
public class OrderController {

    @Resource
    private IOrderService orderService;

    /**
     * 创建订单
     */
    @GetMapping("/order/create")
    public ResultData create(Order order)
    {
        orderService.create(order);
        return ResultData.success(order);
    }

}

OrderServiceImpl

/**
 * @author author
 * @since 2024-04-15
 * 下订单->减库存->扣余额->改(订单)状态
 */
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {

    @Resource
    private OrderMapper orderMapper;

    /**
     * 订单微服务通过OpenFeign去调用库存微服务
     */
    @Resource
    private StorageFeignApi storageFeignApi;

    /**
     * 订单微服务通过OpenFeign去调用账户微服务
     */
    @Resource
    private AccountFeignApi accountFeignApi;


    @Override
//    @GlobalTransactional(name = "swx-create-order",rollbackFor = Exception.class)
    public void create(Order order) {

        //xid检查
        String xid = RootContext.getXID();

        //1. 新建订单
        log.info("==================>开始新建订单"+"\t"+"xid_order:" +xid);
        //订单状态status:0:创建中;1:已完结
        order.setStatus(0);
        int result = orderMapper.insert(order);

        //插入订单成功后获得插入mysql的实体对象
        Order orderFromDB = null;
        if(result > 0)
        {
            QueryWrapper<Order> wrapper = new QueryWrapper<>();
            wrapper.eq("status", order.getStatus());
            orderFromDB = orderMapper.selectOne(wrapper);
            //orderFromDB = orderMapper.selectByPrimaryKey(order.getId());
            log.info("-------> 新建订单成功,orderFromDB info: "+orderFromDB);
            System.out.println();
            //2. 扣减库存
            log.info("-------> 订单微服务开始调用Storage库存,做扣减count");
            storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
            log.info("-------> 订单微服务结束调用Storage库存,做扣减完成");
            System.out.println();
            //3. 扣减账号余额
            log.info("-------> 订单微服务开始调用Account账号,做扣减money");
            accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
            log.info("-------> 订单微服务结束调用Account账号,做扣减完成");
            System.out.println();
            //4. 修改订单状态
            //订单状态status:0:创建中;1:已完结
            log.info("-------> 修改订单状态");
            orderFromDB.setStatus(1);
            int updateResult = baseMapper.updateById(orderFromDB);

            log.info("-------> 修改订单状态完成"+"\t"+updateResult);
            log.info("-------> orderFromDB info: "+orderFromDB);
        }
        System.out.println();
        log.info("==================>结束新建订单"+"\t"+"xid_order:" +xid);
    }
}
新建库存storage微服务
  • 建Model

    • seata-storage-service2002
  • 改pom

    <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
        <dependencies>
            <!--自定义-->
            <dependency>
                <groupId>org.cloud</groupId>
                <artifactId>cloud-commons</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--openfeign-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <!--loadbalancer-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
            <!--alibaba-seata-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            </dependency>
            <!--nacos-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!--SpringBoot通用依赖模块-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <!--SpringBoot集成druid连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
            </dependency>
            <!--mybatis-plus和springboot整合-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            </dependency>
            <!--Mysql数据库驱动8 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <!--persistence-->
            <dependency>
                <groupId>javax.persistence</groupId>
                <artifactId>persistence-api</artifactId>
            </dependency>
            <!--hutool-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
            </dependency>
            <!-- fastjson2 -->
            <dependency>
                <groupId>com.alibaba.fastjson2</groupId>
                <artifactId>fastjson2</artifactId>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.28</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
  • 写yml

    server:
      port: 2002
    
    spring:
      application:
        name: seata-storage-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848         #Nacos服务注册中心地址
      # ==========applicationName + druid-mysql8 driver===================
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
        username: root
        password: '020708'
    # ========================mybatis===================
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      type-aliases-package: org.cloud.entity
      configuration:
        map-underscore-to-camel-case: true
    # ========================seata===================
    seata:
      registry:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          namespace: ""
          group: SEATA_GROUP
          application: seata-server
      tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
      service:
        vgroup-mapping:
          default_tx_group: default # 事务组与TC服务集群的映射关系
      data-source-proxy-mode: AT
    
    logging:
      level:
        io:
          seata: info
    
  • 启动类

    @SpringBootApplication
    @MapperScan("org.cloud.mapper")
    @EnableDiscoveryClient
    @EnableFeignClients
    public class SeataStorageServiceapp
    {
        public static void main( String[] args )
        {
            System.out.println( "Hello World!" );
        }
    }
    
  • 业务类

    StorageController

@RestController
@RequestMapping("/storage")
public class StorageController {

    @Resource
    private IStorageService storageService;

    /**
     * 扣减库存
     */
    @RequestMapping("/decrease")
    public ResultData decrease(Long productId, Integer count) {
        storageService.decrease(productId, count);
        return ResultData.success("扣减库存成功!");
    }
}

StorageServiceImpl

@Slf4j
@Service
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements IStorageService {

    @Resource
    private StorageMapper storageMapper;

    /**
     * 扣减库存
     */
    @Override
    public void decrease(Long productId, Integer count) {
        log.info("------->storage-service中扣减库存开始");
        storageMapper.decrease(productId,count);
        log.info("------->storage-service中扣减库存结束");
    }
}

StorageMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.cloud.mapper.StorageMapper">
    <resultMap id="BaseResultMap" type="org.cloud.model.entity.Storage">
        <id column="id" jdbcType="BIGINT" property="id" />
        <result column="product_id" jdbcType="BIGINT" property="productId" />
        <result column="total" jdbcType="INTEGER" property="total" />
        <result column="used" jdbcType="INTEGER" property="used" />
        <result column="residue" jdbcType="INTEGER" property="residue" />
    </resultMap>

    <update id="decrease">
        UPDATE
            t_storage
        SET
            used = used + #{count},
            residue = residue - #{count}
        WHERE product_id = #{productId}
    </update>
</mapper>
新建账户Account微服务
  • 建Model

    • seata-account-service2003
  • 改pom

    <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
        <dependencies>
            <!--自定义-->
            <dependency>
                <groupId>org.cloud</groupId>
                <artifactId>cloud-commons</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--openfeign-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <!--loadbalancer-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
            <!--alibaba-seata-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            </dependency>
            <!--nacos-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!--SpringBoot通用依赖模块-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <!--SpringBoot集成druid连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
            </dependency>
            <!--mybatis-plus和springboot整合-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            </dependency>
            <!--Mysql数据库驱动8 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <!--persistence-->
            <dependency>
                <groupId>javax.persistence</groupId>
                <artifactId>persistence-api</artifactId>
            </dependency>
            <!--hutool-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
            </dependency>
            <!-- fastjson2 -->
            <dependency>
                <groupId>com.alibaba.fastjson2</groupId>
                <artifactId>fastjson2</artifactId>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.28</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
  • 写yml

    server:
      port: 2003
    
    spring:
      application:
        name: seata-account-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848         #Nacos服务注册中心地址
      # ==========applicationName + druid-mysql8 driver===================
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
        username: root
        password: '020708'
    # ========================mybatis===================
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      type-aliases-package: org.cloud.entity
      configuration:
        map-underscore-to-camel-case: true
    
    
    
    # ========================seata===================
    seata:
      registry:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          namespace: ""
          group: SEATA_GROUP
          application: seata-server
      tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
      service:
        vgroup-mapping:
          default_tx_group: default # 事务组与TC服务集群的映射关系
      data-source-proxy-mode: AT
    
    logging:
      level:
        io:
          seata: info
    
  • 启动类

    @SpringBootApplication
    @MapperScan("org.cloud.mapper")
    @EnableDiscoveryClient
    @EnableFeignClients
    public class SeataAccountServiceApp
    {
        public static void main( String[] args )
        {
            SpringApplication.run(SeataAccountServiceApp.class,args);
        }
    }
    
  • 业务类

    AccountController

@RestController
@RequestMapping("/account")
public class AccountController {
    @Resource
    private IAccountService accountService;

    /**
     * 扣减账户余额
     */
    @RequestMapping("/decrease")
    public ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money){
        accountService.decrease(userId,money);
        return ResultData.success("扣减账户余额成功!");
    }
}

AccountServiceImpl

@Slf4j
@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {

    @Resource
    AccountMapper accountMapper;

    /**
     * 扣减账户余额
     */
    @Override
    public void decrease(Long userId, Long money) {
        log.info("------->account-service中扣减账户余额开始");

        accountMapper.decrease(userId,money);

        //myTimeOut();
        //int age = 10/0;
        log.info("------->account-service中扣减账户余额结束");
    }

    /**
     * 模拟超时异常,全局事务回滚
     */
    private static void myTimeOut()
    {
        try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

AccountMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.cloud.mapper.AccountMapper">
    <resultMap id="BaseResultMap" type="org.cloud.model.entity.Account">
        <id column="id" jdbcType="BIGINT" property="id" />
        <result column="user_id" jdbcType="BIGINT" property="userId" />
        <result column="total" jdbcType="DECIMAL" property="total" />
        <result column="used" jdbcType="DECIMAL" property="used" />
        <result column="residue" jdbcType="DECIMAL" property="residue" />
    </resultMap>


    <!--
        money   本次消费金额

        t_account数据库表
        total总额度 = 累计已消费金额(used) + 剩余可用额度(residue)
    -->
    <update id="decrease">
        UPDATE
            t_account
        SET
            residue = residue - #{money},used = used + #{money}
        WHERE user_id = #{userId};
    </update>
</mapper>

案例实战测试

启动服务
  • 启动Nacos
  • 启动Seata
  • 启动openfeign服务
  • 启动订单微服务2001
  • 启动库存微服务2002
  • 启动账户微服务2003
数据库初始情况
c392fd8d9746436d0847dd863e6b2275.png
正常下单

此时我们没有在订单模块添加==@GlobalTransactional==

结果:没有任何问题

超时异常出错

此时我们没有在订单模块添加==@GlobalTransactional==

故障情况:

当库存和账户全额扣减后订单状态并没有设置为已经完成。没有从零改为1

数据库情况

56d17268016290569c5dc4153036b638.png
超时异常解决

此时我们在订单模块添加==@GlobalTransactional==

@Override
@GlobalTransactional(name = "swx-create-order",rollbackFor = Exception.class) //AT
public void create(Order order)

{
    ..........
}

此时,订单模块就是TM,也是其中一个RM

06d04b1d5b58a7378643d845cac88f10.png

查看Seata后台

http://IocaIhost:7091/#/Iogin

  • 全局事务ID

    • 9ac402fe5082306cc69ab61b3dd93e70.png
  • 全局锁

    • 272d01586b4b51d049fd5fbaba202b9e.png

下单后数据库并没有任何变化,被回滚了

  • 业务中
41487e080d4a57d58b9a82c2ceff476c.png
41487e080d4a57d58b9a82c2ceff476c.png

按照正常逻辑,本该有新记录入库,等待最后完成提交。

  • 回滚后

    • Order记录根本就插入不进来

      5172cdaac923752a846fbe48f780eaf5.png
      5172cdaac923752a846fbe48f780eaf5.png

上一步准备新增的记录被彻底回滚了,保证的一致性。

全部回退

总结

AT模式如何做到对业务的无侵入

是什么?
c74b2569dfde2c5e305c1dfdb4d8e35b.png
一阶段加载

在一阶段,Seata 会拦截“业务 SQL”,

1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,

2 执行“业务 SQL”更新业务数据,在业务数据更新之后,

3 其保存成“after image”,最后生成行锁。

以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

7cdfe20fdd64ca75ca16626ea1d36c93.png
7cdfe20fdd64ca75ca16626ea1d36c93.png
二阶段分2种情况
正常提交

二阶段如是顺利提交的话,

因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

8f2efadea795bcde57fe92734ef08a34.png
8f2efadea795bcde57fe92734ef08a34.png
异常回滚

二阶段回滚:

二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。

回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,

如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

717a62a80ba4d36d5dfee1f25835c885.png
717a62a80ba4d36d5dfee1f25835c885.png