分布式系列(一)SprintBoot集成Seata 2PC实现分布式事务

SprintBoot集成Seata 2PC实现分布式事务


我们的业务场景是Admin管理员记录日志、调用订单微服务发起一个订单、商品根据订单的数量减少相应的商品
管理员的日志、发起订单、货物减少 这是3个系统的交互,我们要保证分布式事务的一致性。

所以我们有3个微服务,Admin记录操作、Order记录订单、Goods删减货物
1.2PC的原理
1.1 2PC概念

两阶段提交又称2PC,两个阶段:第一阶段:投票阶段 和第二阶段:提交/执行阶段。

举例 订单服务A,需要调用 支付服务B 去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理为失败状态。

1.2 2PC第一阶段

在这里插入图片描述
第一阶段分3步

  1. 事务询问
    协调者 向所有的 参与者 发送事务预处理请求,称之为Prepare,并开始等待各 参与者 的响应
  2. 协调者
    各个 参与者 节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向 协调者 报告说:“我准备好了/我不行”。
  3. 各参与者向协调者反馈事务询问的响应
    如果 参与者 成功执行了事务操作,那么就反馈给协调者 Yes 响应,表示事务可以执行
    如果没有 参与者 成功执行事务,那么就反馈给协调者 No 响应,表示事务不可以执行

第一阶段执行完后,会有两种可能。1、所有都返回Yes. 2、有一个或者多个返回No。

1.3 2PC第二阶段

在这里插入图片描述
第二阶段分2步

  1. 所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交
    ​ 协调者 向 所有参与者 节点发出Commit请求
  2. 事务提交
    ​ 参与者 收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源
1.4 2PC异常处理

在这里插入图片描述
异常处理也分为2步

  1. 发送回滚请求
    ​ 协调者向所有参与者节点发出 RoollBack 请求
  2. 事务回滚
    参与者 接收到RoollBack请求后,会回滚本地事务
1.5 2PC的缺点

1)性能问题

无论是在第一阶段的过程中,还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有当所有节点准备完毕,事务 协调者 才会通知进行全局提交,

参与者 进行本地事务提交后才会释放资源。加锁过程持续了2个阶段,这样的过程会比较漫长,对性能影响比较大。

2)单节点故障

由于协调者的重要性,一旦 协调者 发生故障。参与者 会一直阻塞下去。尤其在第二阶段,协调者 发生故障,那么所有的 参与者 还都处于锁定事务资源的状态中,而无法继续完成事务操作

2.Seata的原理

传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。

主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供AT模式(即2PC)及TCC模式的分布式事务解决方案。

2.1Seata组成

与 传统2PC 的模型类似,Seata定义了3个组件来协议分布式事务的处理过程:
在这里插入图片描述

  • Transaction Coordinator (TC): 事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运 行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚
  • Transaction Manager(TM): 事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终 向TC发起全局提交或全局回滚的指令
  • Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分 支(本地)事务的提交和回滚
2.2 Seata事务处理流程

在这里插入图片描述
具体的执行流程如下:

  1. 用户服务的 TM 向 TC 申请开启一个全局事务,注意只有用户TM、没有积分TM,因为TM事务管理者只有一个发起者,发起全局事务的开启,全局事务创建成功并生成一个全局唯一的XID。
  2. 用户服务的 RM 向 TC 注册 分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局事务的管辖。
  3. 用户服务执行分支事务,向用户表插入一条记录,注意此处事务已经提交、资源得到释放,此处解决2pc性能问题。
  4. 逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的RM 向 TC 注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。
  5. 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务,注意此处事务也同样提交,资源得到释放,解决2pc性能问题。
  6. 用户服务分支事务执行完毕。
  7. TM 向 TC 发起针对 XID 的全局提交或回滚决议,提交的事务回滚其实就是对应的做减法操作,增加记录对应删减记录。
  8. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
2.3 Seata实现2PC与传统2PC差别

架构层次方面:

  • 传统2PC方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现
  • Seata的 RM 是以jar包的形式作为中间件层部署在应用程序这一侧的。

两阶段提交方面

  • 传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成才释放。
  • Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2持锁的时间,解决性能问题,整体提高效率。
3.SpringBoot集成Seata
本次实战 使用的是SpringBoot 2.4.4 + Zookeeper 3.5.3 + Dubbo 2.7.7 
Seata 1.4.1 (seata-spring-boot-starter)+ spring-cloud-starter-alibaba-seata 2.4.4
3.1 Seata 配置文件
3.1.1 Seata 下载

可以从链接下载Seata的压缩包
链接:https://pan.baidu.com/s/1hE4N6O8tl4b0zTY-i4jThQ
提取码:h1lh

解压文件 到本地文件夹,比如我的就是 E:\setup\install\seata
然后进入seata 文件夹 E:\setup\install\seata\conf 可以看到 两个配置文件
file.conf及register.conf
在这里插入图片描述

3.1.2 Seata 配置模式及注册方式

下面我们配置下 file.conf
我们可以看到 seata的存储模式有以下几种,store mode: file、db、redis
这次我们选择db配置方式,配置DB模式,其他的 file、redis的配置可以不变,修改db下面的配置,填写自己的地址、数据库、及账号密码

!!!! 有两个地方注意下

  1. 我本地装的Mysql8.0 ,所以mysql的驱动 Connect的jar包 是有问题的
    解决办法:需要将mysql8.0的依赖包 mysql-connector-java-8.0.23.jar 复制到seata的安装目录下面的lib 依赖包目录下,比如我的就是E:\setup\install\seata\lib
    在这里插入图片描述

2.Mysql8.0的驱动配置 driverClassName 是和mysql5.x不一样的
解决办法:file.conf中db下面的驱动配置
driverClassName = “com.mysql.cj.jdbc.Driver” 要这样配置,才能生效

file.conf 配置文件如下:

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.cj.jdbc.Driver"
    url = "jdbc:mysql://localhost:3306/seata"
    user = "root"
    password = "root"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

  ## redis store property
  redis {
    host = "127.0.0.1"
    port = "6379"
    password = ""
    database = "0"
    minConn = 1
    maxConn = 10
    maxTotal = 100
    queryLimit = 100
  }

}

下面我们配置下registry.conf
注册中心的配置,我们是使用的Zookeeper
我们可以看到seata的注册中心有多种 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa,这次我们选用zk,修改register.conf配置文件中的type类型选择zk及zk下面的配置

registry.conf 配置如下

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "zk"
  loadBalance = "RandomLoadBalance"
  loadBalanceVirtualNodes = 10

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = ""
    password = ""
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
    apolloAccesskeySecret = ""
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

3.1.3 Seata建库建表

从file.conf中的配置可知,我们是有一个seata库的,新建seata数据库,库中有四个表
建表语句

/*
 Navicat MySQL Data Transfer

 Source Server         : localhosst
 Source Server Type    : MySQL
 Source Server Version : 80022
 Source Host           : localhost:3306
 Source Schema         : seata

 Target Server Type    : MySQL
 Target Server Version : 80022
 File Encoding         : 65001

 Date: 22/04/2021 22:26:27
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table`  (
  `branch_id` bigint NOT NULL,
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` bigint NULL DEFAULT NULL,
  `resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `status` tinyint NULL DEFAULT NULL,
  `client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime(6) NULL DEFAULT NULL,
  `gmt_modified` datetime(6) NULL DEFAULT NULL,
  PRIMARY KEY (`branch_id`) USING BTREE,
  INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for global_table
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table`  (
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` bigint NULL DEFAULT NULL,
  `status` tinyint NOT NULL,
  `application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `timeout` int NULL DEFAULT NULL,
  `begin_time` bigint NULL DEFAULT NULL,
  `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`xid`) USING BTREE,
  INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
  INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table`  (
  `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_id` bigint NULL DEFAULT NULL,
  `branch_id` bigint NOT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`row_key`) USING BTREE,
  INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `branch_id` bigint NOT NULL,
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

!!! 注意 Seata 的分布式事务表 undo_log是需要每个库都有的,比如我现在是管理员微服务、订单微服务、货物微服务 分别对应的数据库是 admin、order、goods三个数据库,undo_log是要每个微服务的数据库都有的
在这里插入图片描述

3.1.4 启动Seata

在安装目录bin下面有个 seata-server.bat 现在我们启动它,指定端口8091及模式db

 seata-server.bat -p 8091 -h 127.0.0.1 -m db

服务正常启动
在这里插入图片描述

3.2 项目配置
3.2.1 项目Pom引入及properties配置

Pom.xml中加入seata的依赖,主要是下面部分
在这里插入图片描述
Pom文件的配置如下:

<?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.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.jzj</groupId>
    <artifactId>admin</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>admin</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>8</java.version>
        <dubbo.version>2.7.7</dubbo.version>
        <curator.version>4.0.1</curator.version>
    </properties>
    <dependencies>
        <!--		引入Order服务-->
        <dependency>
            <groupId>com.jzj</groupId>
            <artifactId>order-client</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!--		引入Goods服务-->
        <dependency>
            <groupId>com.jzj</groupId>
            <artifactId>goods-client</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions><!-- 去掉springboot log默认配置 -->
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 引入log4j2依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--自启动Druid管理后台-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator</artifactId>
            <version>1.3.5</version>
            <type>pom</type>
        </dependency>
        <!--分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.4</version>
        </dependency>
        <!--tkmybatis -->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>


        <!--添加Dubbo及Zookeeper、zkclient依赖		-->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>${dubbo.version}</version>
        </dependency>

        <!--        zookeeper客户端注册中心依赖-->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>${curator.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>${curator.version}</version>
        </dependency>

        <!--        java 工具类-->
        <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.6.3</version>
        </dependency>


        <!--        seata 依赖-->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-seata -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <version>2.2.4.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </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>

application.properties中加入seata的配置,主要以下部分,注意自定义分组
在这里插入图片描述
全部配置文件如下所示

server.port=9601
spring.application.name=adminApplication
#Dubbo

dubbo.protocol.name=dubbo
dubbo.application.name=adminApplication
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.consumer.check=false
dubbo.registry.group=trans


#log
logging.config=classpath:log4j/log4j2-test.xml

#mq
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.addresses=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.listener.simple.acknowledge-mode=manual

#db
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/admin?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

#连接池的设置
#初始化时建立物理连接的个数
spring.datasource.druid.initial-size=5
#最小连接池数量
spring.datasource.druid.min-idle=5
#最大连接池数量 maxIdle已经不再使用
spring.datasource.druid.max-active=20
#获取连接时最大等待时间,单位毫秒
spring.datasource.druid.max-wait=60000
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
spring.datasource.druid.test-while-idle=true
#既作为检测的间隔时间又作为testWhileIdel执行的依据
spring.datasource.druid.time-between-eviction-runs-millis=60000
#销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接
spring.datasource.druid.min-evictable-idle-time-millis=30000
#用来检测连接是否有效的sql 必须是一个查询语句
#mysql中为 select 'x'
#oracle中为 select 1 from dual
spring.datasource.druid.validation-query=select 'x'
#申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
spring.datasource.druid.test-on-borrow=false
#归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
spring.datasource.druid.test-on-return=false
#当数据库抛出不可恢复的异常时,抛弃该连接
spring.datasource.druid.exception-sorter=true
#是否缓存preparedStatement,mysql5.5+建议开启
#spring.datasource.druid.pool-prepared-statements=true
#当值大于0时poolPreparedStatements会自动修改为true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
#配置扩展插件
spring.datasource.druid.filters=stat,wall
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.druid.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
#合并多个DruidDataSource的监控数据
spring.datasource.druid.use-global-data-source-stat=true
#设置访问druid监控页的账号和密码,默认没有
#spring.datasource.druid.stat-view-servlet.login-username=admin
#spring.datasource.druid.stat-view-servlet.login-password=admin
druid.login.user_name=root
druid.login.password=root


#mybatis 不需要配置Mapper.xml
mybatis.type-aliases-package=com.jzj.admin.bean.entity
mapper.mappers=com.jzj.admin.mybatis.base.BaseMapper
mapper.identity=MYSQL

#配置seata
seata.enabled=true
seata.application-id=#{spring.application.name}
#seata的各个服务要用一个分组,否组不生效
seata.tx-service-group=jzj_test_seata_group
seata.enable-auto-data-source-proxy=true
seata.service.vgroup-mapping.jzj_test_seata_group=default
seata.service.grouplist.default=127.0.0.1:8091
3.2.2 项目Admin Service

注意我们是微服务,所以Admin是我们的一个管理员微服务、Order是我们的另一个订单微服务、Goods是我们的一个货物微服务
我们操作流程是 Admin发起全局事务,作为TM的事务管理发起者

  1. 管理员发起事务、记录日志、调用订单微服务的RPC接口,进行下单
  2. 订单微服务的RPC接口下单,处理下单业务,Order微服务在下单完成后
  3. 货物微服务根据下单数量,去库存中减少货物数量,如果库存不够,下单失败,所有操作全部回滚

所以这三步是一个流程,管理员操作日志没写入、或者订单下单失败、或者货物存储不够,全部要回滚操作,这才是一个完整的事务
下面我们看下Admin 管理员中的操作
在这里插入图片描述
分析一下代码

  1. 方法上加上全局事务开启 @GlobalTransactional
  2. 第一步 管理员开启日志
  3. 第二部 掉订单微服务RPC接口,下订单
  4. 第三步 掉货物微服务RPC接口,减少货物数量

任何一个过程出错,都会回滚

3.2.3 订单、货物微服务实现

OrderRpcService实现及接口
在这里插入图片描述
OrderRpcServiceImpl.java

package com.jzj.order.web.service.rpcimpl;

import cn.hutool.json.JSONUtil;
import com.jzj.order.client.IOrderRpcService;
import com.jzj.order.client.bean.OrderModel;
import com.jzj.order.web.bean.entity.OrderPO;
import com.jzj.order.web.mybatis.dao.IOrderDAO;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;

import javax.annotation.Resource;


@DubboService(interfaceClass = IOrderRpcService.class)
@Slf4j
public class OrderRpcServiceImpl implements IOrderRpcService {


    @Resource
    private IOrderDAO orderDAO;

    @Override
    public OrderModel getOrderInfo(Integer orderId) {
        OrderPO orderPO = orderDAO.selectByPrimaryKey(orderId);

        OrderModel model = OrderModel.builder()
                .orderName(orderPO.getOrderName())
                .payNumber(orderPO.getPayNumber())
                .id(orderPO.getId()).build();
        log.info("查询 order:{}", JSONUtil.toJsonStr(model));
        return model;
    }

    @Override
    @Transaction
    public void addOrder(OrderModel orderModel) {
        log.info("Order 全局事务,XID = " + RootContext.getXID());
        OrderPO orderPO = OrderPO.builder()
                .orderName(orderModel.getOrderName())
                .payNumber(orderModel.getPayNumber()).build();
        int count = orderDAO.insertUseGeneratedKeys(orderPO);
        if (count > 0) {
            log.info("添加成功 order:{}", JSONUtil.toJsonStr(orderPO));
            return;
        }
        log.info("添加失败 order:{}", JSONUtil.toJsonStr(orderModel));
    }
}

同样的Goods货物微服务,GoodsRpcService接口及实现
在这里插入图片描述
GoodsRpcServiceImpl.java

package com.jzj.goods.web.service.rpcimpl;

import cn.hutool.json.JSONUtil;
import com.jzj.goods.client.GoodsModel;
import com.jzj.goods.client.IGoodRpcService;
import com.jzj.goods.web.bean.entity.GoodsPO;
import com.jzj.goods.web.mybatis.dao.IGoodsDAO;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;

import javax.annotation.Resource;


@DubboService(interfaceClass = IGoodRpcService.class)
@Slf4j
public class GoodsRpcServiceImpl implements IGoodRpcService {


    @Resource
    private IGoodsDAO goodsDAO;

    @Override
    public GoodsModel getGoodsInfo(Integer goodsId) {
        GoodsPO goodsPO = goodsDAO.selectByPrimaryKey(goodsId);

        GoodsModel model = GoodsModel.builder()
                .goodsName(goodsPO.getGoodsName())
                .price(goodsPO.getPrice())
                .id(goodsPO.getId()).build();
        log.info("查询 goods:{}", JSONUtil.toJsonStr(model));
        return model;
    }

    @Override
    public void addGoods(GoodsModel goodsModel) {
        GoodsPO goodsPO = GoodsPO.builder()
                .goodsName(goodsModel.getGoodsName())
                .price(goodsModel.getPrice()).build();
        int count = goodsDAO.insertUseGeneratedKeys(goodsPO);
        if (count > 0) {
            log.info("添加成功 goods:{}", JSONUtil.toJsonStr(goodsPO));
            return;
        }
        log.info("添加失败 goods:{}", JSONUtil.toJsonStr(goodsModel));
    }


    @Override
    @Transaction
    public void deleteGoods(Integer goodsId, Integer goodsStore) {
        log.info("Goods 全局事务,XID = " + RootContext.getXID());
        GoodsPO oldGoodsPO = goodsDAO.selectByPrimaryKey(goodsId);
        Integer resultGoodsStore = oldGoodsPO.getStore() - goodsStore;
        //设置货物减少数目
        if (resultGoodsStore < 0) {
            log.error("减少货物失败 货物库存不足");
            throw new RuntimeException("减少货物失败 货物库存不足");
        }
        oldGoodsPO.setStore(resultGoodsStore);

        goodsDAO.updateByPrimaryKey(oldGoodsPO);
    }
}

3.3 测试
3.3.1 启动3个微服务

我们在Admin中新增一个Conrtoller用于触发这个流程

    @RequestMapping("buy")
    public Object buy(Integer number){
        String msg = "root buy "+ number+" someThings";
        log.info("管理员准备买东西===============================");
       buyService.adminBusGoods(msg, number);
        return "ok";
    }

启动Admin、Order、Goods三个微服务,我们以Admin为例,看下启动日志

#表明 buyServiceImpl 上有全局事务开启标志
[2021-04-22 23:04:44:308]  [,] INFO [timeoutChecker_1_1] io.seata.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:TMROLE,address:127.0.0.1:8091,msg:< RegisterTMRequest{applicationId='#{spring.application.name}', transactionServiceGroup='jzj_test_seata_group'} >

#TM 事务管理器已经注册成功
[2021-04-22 23:04:44:322]  [,] INFO [timeoutChecker_1_1] io.seata.core.rpc.netty.TmNettyRemotingClient : register TM success. client version:1.4.1, server version:1.4.1,channel:[id: 0x0326e598, L:/127.0.0.1:52343 - R:/127.0.0.1:8091]

在这里插入图片描述

3.3.2 成功运行

请求admin Controller触发流程,购买1个货物

请求 http://localhost:9601/test/buy?number=1

Admin管理员日志
在这里插入图片描述
分析:

  1. 开启全局事务 Begin new global transaction [169.254.177.158:8091:128635569157300224]
  2. 全局事务ID Admin 开始全局事务,XID = 169.254.177.158:8091:128635569157300224
  3. 最终状态时第二阶段PhaseTwo 提交 Branch commit result: PhaseTwo_Committed

Order订单微服务日志
在这里插入图片描述
分析:

  1. 有一个全局事务的XID Order 全局事务,XID = 169.254.177.158:8091:128635569157300224
  2. 通过XID,本地事务有个BranchID rm client handle branch commit process:xid=169.254.177.158:8091:128635569157300224,branchId=128635572042981377
  3. 第二阶段成功提交 Branch commit result: PhaseTwo_Committed

Goods货物微服务日志
在这里插入图片描述
分析:

  1. 有一个全局事务的XID Order 全局事务, Goods 全局事务,XID = 169.254.177.158:8091:128635569157300224
  2. 通过XID,本地事务有个BranchID Branch committing: 169.254.177.158:8091:128635569157300224 128635573733285889
  3. 第二阶段成功提交 Branch commit result: PhaseTwo_Committed

看下结果
在这里插入图片描述

3.3.3 异常运行、回滚

我们注意到 写代码的时候,货物里面有段这样的代码,表示如果下单数量大于当前库存数, 那么就下单失败,抛出异常,按照道理来说,是要全部回滚的,管理员记录、订单列表、货物表全都回滚

 public void deleteGoods(Integer goodsId, Integer goodsStore) {
        log.info("Goods 全局事务,XID = " + RootContext.getXID());
        GoodsPO oldGoodsPO = goodsDAO.selectByPrimaryKey(goodsId);
        Integer resultGoodsStore = oldGoodsPO.getStore() - goodsStore;
        //设置货物减少数目
        if (resultGoodsStore < 0) {
            log.error("减少货物失败 货物库存不足");
            throw new RuntimeException("减少货物失败 货物库存不足");
        }
        oldGoodsPO.setStore(resultGoodsStore);

        goodsDAO.updateByPrimaryKey(oldGoodsPO);
    }

下面我们下单100个,货物只有99个,就会有异常,看下日志及数据库是否回滚


Admin日志,由于异常,Branch Rollbacked result: PhaseTwo_Rollbacked 第二阶段时RollBacked的,执行了回滚操作
在这里插入图片描述
Admin库中并没有操作日志
在这里插入图片描述


OrderService订单日志
注意 日志,添加成功是在插入数据后,打印的,到底这个订单入库成功了没,是否被回滚了???
在这里插入图片描述
查看订单数据库,干净的,并没有新的订单
在这里插入图片描述


Goods货物日志
本来到货物这里就失败了,没提交事务,所以也不需要回滚
在这里插入图片描述
Goods的数据库中,存户数量依旧是 99,并没有改变
在这里插入图片描述


至此,关于SpringBoot 集成Seata的分布式事务 就完毕了,还是好用的,对代码入侵较少,比较优雅

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值