Spring Cloud Alibaba Seata (入门篇)

        随着分布式的兴起,出现了一个问题:目前的数据库都只支持本地数据库事务的ACID特性,面对跨多个数据源时,容易出现“脏读”,回滚不成功等一些问题。Seata正是一种保障分布式事务一致性的解决方案。

目录

一. 简介

        1.1 特色功能

        1.2 架构图

        1.3 解决方案

        1.4 概念介绍

        1.5 工作流程

​编辑

2. Sentinel的下载与安装

3. 项目架构

​编辑

        3.1 数据库准备

        3.2 seata_order数据库中的t_order表结构

        3.3 seata_storage数据库中的t_storage表结构

        3.4 seata_account数据库中的t_account表结构

4. 创建工程

        4.1 父工程依赖管理

        4.2 子项目依赖,application.yml,主启动

        4.3 工具模块

        4.4 seata-order-service模块

        4.5 测试

        5.分布式事务详解(AT模式)

        5.1 @GlobalTransactional

        5.2 整体机制

        5.3 @GlobalLock

        5.4 回滚原理

        6. 总结


一. 简介

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

        1.1 特色功能

        ● 微服务框架支持:目前已支持Dubbo、Spring Cloud、Sofa-RPC、Motan 和 gRPC 等RPC框架,其他框架持续集成中

        ● AT模式:提供无侵入自动补偿的事务模式,目前已支持MySQL、Oracle、PostgreSQL、TiDB、MariaDB、DaMeng、PolarDB-X 2.0、SQLServer。DB2开发中

        ● TCC模式:支持 TCC 模式并可与 AT 混用,灵活度更高

        ● SAGA 模式:为长事务提供有效的解决方案,提供编排式与注解式(开发中)

        ● XA 模式:支持已实现 XA 接口的数据库的 XA 模式,目前已支持MySQL、Oracle和MariaDB

        ● 高可用:支持存算分离的集群模式,计算节点可水平扩展,存储支持数据库和 Redis。Raft集群模式已进入beta验证阶段

        

        1.2 架构图

        1.3 解决方案

        1.4 概念介绍

        ● TC(Transaction Coordinator)事务协调器:Seata本身,全局唯一。负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。

        ● TM(Transaction Manager)事务管理器:标注全局@GlobalTransactional启动入口动作的微服务模块,是事务的发起者,全局唯一。负责定义全局事务的范围,并根据TC维护的全局事务和分支事务状态,做出开启事务、提交事务、回滚事务的决议

         ● RM(Resource Manager)资源管理器:数据库本身,全局可以有多个,并且一个微服务可以既是TM又是RM。负责管理分支事务上的资源,向TC注册分支事务,汇报分支事务状态、驱动分支事务的提交和回滚。

        ● xid 全局事务ID:用于唯一标识一个分布式全局事务,随着任务链传递。xid是分布式事务管理的核心,seata通过xid保证事务的提交和回滚,确保数据的一致性和完整性。

        1.5 工作流程

        1.TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID

        2.XID 在微服务调用链路的上下文中传播

        3.RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖

        4. TM 向 TC 发起针对 XID 的全局提交或回滚决议

        5.TC调度XID下管辖的全部分支事务完成提交或回滚请求​

2. Sentinel的下载与安装

 在使用Sentinel前请先确保可以进行Nacos和OpenFeign的基本使用,不会的先看我之前的文章

Nacos下载安装与整合SpringBoot快速入门(服务注册,服务配置中心)-CSDN博客

Nacos整合OpenFeign入门使用-CSDN博客

官方下载地址

官方教程

下载完成,并解压后进入conf目录下,推荐先备份一份系统默认的application.yml

修改application.yml文件,进行个性化配置(连接nacos,连接数据库)

#  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.

 

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

 

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   #mysql8的驱动,如果是8以下记得去掉cj

      url: jdbc:mysql://localhost:3306/你的数据库名字?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true

      user: 你的Mysql用户

      password: 你的Mysql密码

      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   #用于生成jwt密钥

    tokenValidityInMilliseconds: 1800000   #有效期

    ignore:

      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**  #静态资源放行

 

配置完成并保存后先启动Nacos

启动Nacos完成后,进入seata的bin目录下,点击或cmd窗口运行seata-server.bat命令

尝试访问localhost:8848和localhost:7091

出现如上页面代表Seata启动成功并注册进Nacos了,默认用户名和密码都是seata

 最后seata需要在数据库里建表,执行以下sql语句即可

-- -------------------------------- The script used when storeMode is 'db' --------------------------------

-- the table to store GlobalSession data
CREATE DATABASE seata;



USE seata;

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);

3. 项目架构

作为入门篇,本文仅讲解AT模式与Mysql的整合

        本文中将以下图为项目架构图,实现一个分布式交易项目来演示Seata的使用方案与作用。

        3.1 数据库准备

        应用seata AT模式的所有数据库中必须准备一张undo_log表记录回滚信息(后续会展开介绍)

-- 注意此处0.7.0+ 增加字段 context
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

        3.2 seata_order数据库中的t_order表结构

CREATE TABLE `t_order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint DEFAULT NULL COMMENT '用户id',
  `product_id` bigint DEFAULT NULL COMMENT '产品id',
  `count` int DEFAULT NULL COMMENT '数量',
  `money` decimal(11,0) DEFAULT NULL COMMENT '金额',
  `status` int DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8

        3.3 seata_storage数据库中的t_storage表结构

CREATE TABLE `t_storage` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `product_id` bigint DEFAULT NULL COMMENT '产品id',
  `total` int DEFAULT NULL COMMENT '总库存',
  `used` int DEFAULT NULL COMMENT '已用库存',
  `residue` int DEFAULT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

        3.4 seata_account数据库中的t_account表结构

CREATE TABLE `t_account` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` bigint 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 '剩余可用额度',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

4. 创建工程

        4.1 父工程依赖管理

        负责管理所有子项目的默认版本依赖

        请一定注意版本!SpringCloud容易和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>

    <groupId>com.haishi</groupId>
    <artifactId>NacosTest</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>commons</module>
        <module>seata-account-service2003</module>
        <module>seata-order-service2001</module>
        <module>seata-storage-service2002</module>
    </modules>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>3.0.2</spring-boot.version>
        <spring.cloud.alibaba.version>2023.0.1.2</spring.cloud.alibaba.version>
        <lombok.version>1.18.26</lombok.version>
        <druid.version>1.1.20</druid.version>
        <mybatis.springboot.version>3.0.2</mybatis.springboot.version>
        <mysql.version>8.0.11</mysql.version>
        <mapper.version>4.2.3</mapper.version>
        <spring.cloud.version>2022.0.4</spring.cloud.version>
    </properties>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
                <version>2022.0.0.0-RC2</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring.cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--SpringBoot集成mybatis-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.springboot.version}</version>
            </dependency>
            <!--Mysql数据库驱动8 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <!--SpringBoot集成druid连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <!--通用Mapper4之tk.mybatis-->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper</artifactId>
                <version>${mapper.version}</version>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <optional>true</optional>
            </dependency>
            <!--springcloud 2023.0.0 包含了OpenFeign等版本管理-->
            <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>


</project>

        4.2 子项目依赖,application.yml,主启动

        由于三个微服务的项目依赖,application.yml,主启动近乎一模一样,这里就仅介绍一组了

        pom.xml

<?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>com.haishi</groupId>
        <artifactId>NacosTest</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>seata-storage-service2002</artifactId>

    <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>
        <!-- nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </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>
<!--        cloud_commons_utils-->
        <dependency>
            <groupId>com.haishi</groupId>
            <artifactId>commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--web + actuator-->
        <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和springboot整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--Mysql数据库驱动8 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--通用Mapper4-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

</project>

        application.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: root
# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.cloud.entities
  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("com.haishi.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服务注册和发现
@EnableFeignClients
public class SeataStorageMainApp2002
{
    public static void main(String[] args)
    {
        SpringApplication.run(SeataStorageMainApp2002.class,args);
    }
}

        4.3 工具模块

       依赖

<?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>com.haishi</groupId>
        <artifactId>NacosTest</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>commons</artifactId>

    <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>
        <!--SpringBoot通用依赖模块-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.1.7</version>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>4.0.4</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

</project>

        定义一个全局异常处理器

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 默认全局异常处理。
     * @param e the e
     * @return ResultData
     */
    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultData<String> exception(Exception e) {
        log.error("全局异常信息exception:{}", e.getMessage(), e);
        return ResultData.fail(ReturnCodeEnum.RC500.getCode(),e.getMessage());
    }
}

        创建account模块的OpenFeign远程调用接口

@FeignClient(value = "seata-account-service")
public interface AccountFeignApi
{
    //seata-account-service2003微服务中的业务方法,可自行实现
    //扣减账户余额
    @PostMapping("/account/decrease")
    ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}

        创建storage模块的OpenFeign远程调用接口

@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi
{
    //seata-storage-service2003微服务中的业务方法,可自行实现
     //扣减库存
    @PostMapping(value = "/storage/decrease")
    ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

        由于order模块既是RM又是TM,因此并不需要远程调用接口

        4.4 seata-order-service模块

        在OrderServiceImpl中实现业务逻辑,开启事务,实现订单流程。


@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderMapper orderMapper;

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

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

    //name:事务名称    rollbackFor:发生该异常或其子类,进行回滚
    //@GlobalTransactional代表开启AT模式
    @GlobalTransactional(name = "create-order",rollbackFor = Exception.class) //AT
    @Override
    public void create(Order order) {
        //xid全局事务id  重要!!
        String xid = RootContext.getXID();

        //1. 新建订单
        log.info("==================>开始新建订单"+"\t"+"xid_order:" +xid);
        //订单状态status:0:创建中;1:已完结
        order.setStatus(0);
        int result=orderMapper.insertSelective(order);
        //插入订单成功后获得插入mysql的实体对象
        Order orderFromDB=null;
        if(result>0){
            orderFromDB=orderMapper.selectOne(order);
            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. 修改订单状态
            log.info("-------> 修改订单状态");
            orderFromDB.setStatus(1);

            Example whereConditin = new Example(Order.class);
            Example.Criteria criteria = whereConditin.createCriteria();
            //将当前用户下所有账单完结
            criteria.andEqualTo("userId", orderFromDB.getUserId());
            criteria.andEqualTo("status", 0);
            int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereConditin);
            //这里可以再根据完结的账单数量进一步处理,我这里就直接抛出异常了
            if(updateResult>1){
                throw new RuntimeException("有订单未完成,无法结账");
            }
        }
        log.info("==================>结束新建订单"+"\t"+"xid_order:" +xid);
    }
}

        controller层

@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

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

        4.5 测试

        测试成功!

   

        5.分布式事务详解(AT模式)

        5.1 @GlobalTransactional

        @GlobalTransactional用于开启AT模式,但是如果没有这个注解会发生什么?换句话说,不使用seata这个分布式事务解决方案,仅仅使用@Service自带的@Transactional会发生什么?

        首先让我们把@GlobalTransactional注解注释掉

        修改AccountServiceImpl,制造异常

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {

    @Resource
    private AccountMapper accountMapper;

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

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

}

        然后重启所有微服务

        我们首先查看一下库存表

        账户表

接着创建订单,返回超时异常

查看订单表:没有创建成功

但是account表和storage表却扣款成功

        这说明普通的@Transactional在跨数据源的分布式事务中无法保证事务的原子性,只能做到对服务发起者所控制的单个数据源的回滚。因此,在以上情况中,订单模块出现异常,想要回滚事务,却只能回滚订单数据库的数据,对于仓库模块和账户模块,当程序执行完他们的service方法时,仓库表和账户表的数据就已经提交,无法回滚。

        这是由于@Transactional本身是基于数据库start transaction;rollback ;等指令实现的,而目前的数据库都还不支持分布式事务......也许有一天出现一款稳定的分布式数据库我们就没必要学seata了呢。

        然后让我们开启@GlobalTransactional注解再试一下

        回滚成功

        5.2 整体机制

        明明程序在走出仓库和账户模块的service方法时,便已经向数据库提交本地事务,seata又是如何做到对业务无侵入地完成回滚分布式事务的呢?

        TM会首先向TC申请一个xid,开启全局事务,尝试将xid随调用链传递,将所有RM注册进TC,对于所有RM本地事务,执行以下操作:

        官网中将其分为了两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

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

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

        2.执行业务SQL更新数据

        3.将新快照保存成“after image”

        4.生成行锁(seata的行锁,非数据库行锁)

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

        不过需要注意的是,此处生成的行锁是seata的行锁,也即全局锁,用于在分布式事务中实现读隔离和写隔离。全局锁是由seata管理的一个逻辑锁,只有使用seata的微服务能获取或者感知到这把锁。

        ● 每个分布式事务在一阶段本地事务提交前,必须确保拿到全局锁。

        ● 拿不到全局锁则无法提交本地事务

        ● 拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁

      比如在上述更新表数据中,有两个线程同时想要修改某一行的数据,tx1先获取到了行锁,tx2只有当TX1释放行锁后,TX2才能获取行锁对该行进行修改。

        写隔离:

       

        当然Seata还支持读隔离,seata会对想要读取的数据加上行锁。

        在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。

        如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

        二阶段则会分两种情况

        (1)正常提交

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

        (2)异常回滚

        如果发生异常,seata就需要回滚一阶段本地数据库已经提交的数据

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

        若两份数据一样,就说明没有脏写,开始还原数据。如果不一致就说明发生脏写,需要转人工或编写额外机制处理。

        正常项目架构使用seata框架不会发生脏写问题,只有当seata校验事务的时候,另一个不受seata管理的服务对数据库进行数据修改,或者进行到一半服务器停电等特殊情况会发生该类问题。

        5.3 @GlobalLock

        我们需要去思考一种情况:除了创建订单的服务调用链以外,我们还需要新增一个修改库存价格的功能。库存表仅仅是对库存数据库本地事务的修改,但是如果不注册全局事务,则有可能发生脏写问题。

        注册全局事务无疑是一个重量级操作,那么有没有办法去优化它的效率?

        @GlobalLock提供了对于分布式事务的优化:@GlobalLock并不会像@GlobalTransactional那样去向seata申请一个xid开启全局事务,尝试将xid随调用链传递,它仅仅会获取seata中需要修改或访问数据的全局锁,如果获取不到,则说明当前有事务正在执行,可以重试获取。

        注意事项:

        在使用@GlobalLock注解的时候,我们需要更新之前,在查询方法中添加排它锁

SELECT * FROM t_storage WHERE product_id=#{id} FOR UPDATE

         只有在 SQL 中添加了 FOR UPDATE,Seata 才会对数据库行加上排他锁,确保在事务失败时本地锁能及时释放,允许全局事务回滚或重试。如果不添加 FOR UPDATE,Seata 可能无法及时释放本地锁,导致全局事务在回滚时无法立即获取全局锁,从而造成长时间等待,直到 @GlobalLock 所在的事务超时失败才能拿到本地锁并释放全局锁,影响系统的并发性。

        5.4 回滚原理

       那么具体seata是如何控制已经提交的本地事务进行回滚的呢?

        相信很多熟悉MVCC的小伙伴在见到undo_log这个表名的时候便已经有所猜测。没错!seata正是效仿了MVCC的事务机制完成分布式事务ACID特性的保障 ,利用开启事务前后数据的全量快照实现。

        在执行事务的时候,TM会向TC申请开启全局事务,获取xid,并随任务链传递将任务链全部注册进TC。

        如前面所言,seata在一阶段会保存一份“before image”和 "after image"快照,成功获取行锁提交本地事务前,还会将一条日志插入undo_log中,用于后续全局事务的管理

        除去branch_id,xid等全局事务标识外,rollback_info中存储了"before image"和"after image"等信息,用于辅助全局事务的提交或回滚。

        内部信息格式大致是这样:

{
	"branchId": 641789253,
	"undoItems": [{
		"afterImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "GTS"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"beforeImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "TXC"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"sqlType": "UPDATE"
	}],
	"xid": "xid:xxx"
}

        当TM发起全局事务回滚的请求时,seata就能通过"before image"回复数据的原始状态

        或者当TM发起全局事务提交的请求时,seata就能校验“after image”检测是否发生脏写

        6. 总结

        Seata 是一个开源的分布式事务解决方案,旨在解决微服务架构下分布式系统中的事务一致性问题。它通过提供全局事务的管理能力,帮助开发者确保多个分布式服务调用的原子性和一致性。以下是 Seata 的主要功能:

        ● 分布式事务管理

        ● 全局事务的生命周期管理

        ● AT 模式

        ● 全局锁管理

        ● 扩展性和可插拔设计

        ● 支持多种数据库和中间件

        完整的代码放在了仓库中,可自行获取:wandering_4/Spring Cloud Alibaba (gitee.com)

💞 💞 💞本文也就到此结束了,希望能给大家带来帮助

最近也是来到了秋招的好日子,祝大家春风得意马蹄,斩获自己想要的offer

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值