使用Seata实现分布式事务

811 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情

一、创建项目基本模块

1-1、创建基于Spring Cloud Alibaba服务的seata服务

根据之前创建的Spring Cloud Alibaba服务再次创建一个子父级模块,并且添加相关依赖模块。

1-1-1、父项目pom依赖

Spring Cloud Alibaba父项目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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
    <modules>
        <module>Order</module>
      <module>stock</module>
      <module>order-ribbon</module>
      <module>order-loadbalancer</module>
      <module>order-openfeign</module>
      <module>nacos-config</module>
      <module>order-sentinel</module>
      <module>order-openfeign-sentinel</module>
      <module>seata</module>
   </modules>

   <groupId>com.jony</groupId>
   <artifactId>springcloudalibaba</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>springcloudalibaba</name>
   <description>springcloudalibaba</description>
   <packaging>pom</packaging>


   <properties>
      <java.version>1.8</java.version>
      <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
      <spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
      <spring-cloud-version>Hoxton.SR12</spring-cloud-version>
   </properties>
   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter</artifactId>
      </dependency>

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>
   </dependencies>
   <dependencyManagement>
      <dependencies>
         <!--spring cloud alibaba 的版本管理-->
         <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>
         <!--spring boot 的版本管理-->
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
         <!--spring cloud的版本管理-->
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud-version}</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>

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

</project>

1-1-2、seata项目父级项目pom依赖

创建依赖于上面父项目的seata子服务

seata项目创建pom.xml内容如下(如要添加了jdbc、web、mybatis、mysql驱动、druid连接池,并且添加了mybatis代码生成器):

<?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">
    <parent>
        <artifactId>springcloudalibaba</artifactId>
        <groupId>com.jony</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>

        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <!-- Mybatis-Generator插件,自动生成代码 -->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.5</version>
                <configuration>
                    <configurationFile>${project.basedir}/src/main/resources/generatorConfig.xml</configurationFile>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
                <dependencies>
                    <!--必須要引入数据库驱动-->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <!--必须制定版本-->
                        <version>8.0.22</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>

1-2、创建订单服务

订单服务主要完成对订单的增删改查操作。

1-2-1、代码目录结构

完成对Mapper以及service和Controller的创建 image.png

1-2-1-1、Controller方法

接口方法主要负责对订单的保存 image.png

1-2-2、application.yml配置信息

# 数据源
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.253.131:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #初始化时运行sql脚本
    schema: classpath:sql/schema.sql
    initialization-mode: never
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.253.131:8851
        username: nacos
        password: nacos
    alibaba:
      seata:
        tx-service-group: beijing
  application:
    name: seata-order
#设置mybatis
mybatis:
  mapper-locations: classpath:com/jony/order/mapper/*Mapper.xml
  #config-location: classpath:mybatis-config.xml
  typeAliasesPackage: com.jony.order
  configuration:
    mapUnderscoreToCamelCase: true
server:
  port: 8070

1-3、创建库存服务

1-3-1、代码目录结构

库存服务主要实现对库存的扣减 image.png

1-3-1-1、Controller方法

通过接收商品ID对库存进行扣减 image.png

1-3-1-2、Mapper方法

通过ID对库存进行数量-1操作 image.png

1-3-2、application.yml配置信息

主要对数据、mybatis、nacos进行相关配置

# 数据源
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.253.131:3306/seata_stock?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #初始化时运行sql脚本
    schema: classpath:sql/schema.sql
    initialization-mode: never
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.253.131:8851
        username: nacos
        password: nacos
    alibaba:
      seata:
        tx-service-group: beijing
  application:
    name: seata-stock
#设置mybatis
mybatis:
  mapper-locations: classpath:com/jony/stock/mapper/*Mapper.xml
  #config-location: classpath:mybatis-config.xml
  typeAliasesPackage: com.jony.stock
  configuration:
    mapUnderscoreToCamelCase: true
server:
  port: 8071

1-4、数据库表

对订单和库存服务分别创建各自的数据库表,同时创建各自服务的undo_log信息。 image.png

通过以上操作就完成了订单服务和扣减库存服务的创建,下面看一下通过分布式服务如何进行服务的调用以及使用seata实现分布式事务的处理

二、微服务的调用及分布式事务

上面代码已经完成了各自服务代码功能的创建,但是还未完成服务之间的调用,之前的文章有提到,服务之间调用可以通过Nacos+Feign来处理,下面首先来进行这一步处理,然后再进行微服务分布式事务的处理

2-1、微服务Nacos+Feign的处理

2-1-1、添加nacos、Feign的依赖

<!--nacos 服务注册与发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--openFeign的依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2-1-2、Nacos注册中心相关配置

分别给订单服务和库存服务配置nacos注册中心,同时修改服务名称
订单服务:seata-order
库存服务:seata-stock

application:
  name: seata-order
#nacos注册中心的配置
cloud:
  nacos:
    discovery:
      server-addr: 192.168.253.131:8851
      username: nacos
      password: nacos

2-1-3、在服务调用端创建OpenFeign的接口

当前项目主要是订单调用库存服务,因此需要在订单服务中创建库存服务OpenFeign的调用接口

2-1-3-1、首先在启动类上添加@EnableFeignClients

通过添加@EnableFeignClients,则代表启用feign客户端;扫描和注册feign客户端bean定义 image.png

2-1-3-2、创建stockService的OpenFeign接口

image.png

2-1-3-3、在订单服务service中通过OpenFeign调用库存服务

通过以下操作就可以完成、通过OpenFeign调用库存服务了 image.png

2-2、启动订单和库存服务

将订单和库存服务启动,在Nacos控制台就可以看到两个服务了

image.png

2-3、调用订单服务,通过OpenFeign调用库存服务

2-3-1、访问服务前数据库信息

订单服务目前数据库信息

image.png

库存服务数据数据信息

image.png

启动订单服务和库存服务,然后访问订单接口

image.png

2-3-2、访问服务后数据库信息

订单服务数据库信息 image.png

库存服务信息 image.png

2-3-2、小结

通过以上可以看到:
访问服务前:订单一条数据,库存99
访问服务后:订单二条数据,库存96\

这是因为我本地测试的时候,因为Nacos服务问题导致订单服务异常、库存服务正常。这样订单没成功、然后库存却扣减成功了。这样在分布式服务的场景下肯定是不行的。

三、配置微服务整合Seata

3-1、添加依赖

给分布式事务相关的微服务添加seata依赖

<!-- seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

3-2、给微服务添加undo_log数据库表

在前面的文章中已经讲述过使用seata实现分布式事务的原理,实际就是在事务执行前记录元数据和事务执行后也记录元数据

image.png 如果事务需要回滚则利用存储的元数据进行存储,

image.png 可以看下之前的文章juejin.cn/editor/draf…

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 DEFAULT CHARSET=utf8;

3-3、微服务seata相关配置(订单和库存服务都需要配置)

3-3-1、seata分组配置

3-3-1-1、seata分组的设置

在前面的文章中有提到给seata进行分组,防止区域灾备处理。之前配置的文件在/seata/script/config-center中的config.txt中如下:

image.png

在之前的文章中已经配置为beijing,如下: image.png

3-3-1-2、微服务application.yml中seata分组配置

image.png

3-3-2、微服务中seata服务的注册中心配置

微服务要使用seata,还需要告诉微服务去哪里进行访问seata服务端

在spring cloud alibaba 2.1之前,需要将官方提供的register.conf放到微服务中的resource中。2.1之后在application.yml中进行配置即可(默认值未修改的可以不进行配置)。如下:

image.png

3-3-3、微服务中seata服务的配置中心配置

image.png

3-4、给微服务调用方法添加@GlobalTransactional测试分布式事务

通过以上配置就完成了seata+nacos+feign分布式微服务的搭建,然后分别启动这两个微服务

启动的时候报了一个错误:如下:

no available service 'null' found, please make sure registry config correct

image.png

经过查找终于发现,是因为我把默认的public命名空间改为了seata。但是在配置文件中的namespace不能写seata,必须写seata的dataID.

如下(需要同时将订单和库存服务都修改了):

image.png

修改之后,再次启动就没有问题了。

3-4-1、让两个服务都成功请求

访问前:订单3条,库存96
访问后:订单4条,库存95,访问后数据库数据如下:

订单数据库: image.png

库存数据库: image.png

3-4-2、手动创建异常,查看分布式事务是否可以生效

通过添加执行异常代码,查看事务是否回滚 image.png

访问创建订单接口,可以看到已经发生异常。 image.png

再次查看数据库:
订单数据库:

image.png

库存数据库:

image.png

可以看到程序在异常的情况下,订单服务和库存服务都完成了事务的回滚,这样通过seata来实现分布式事务就圆满完成了。