SprintBoot集成Seata 2PC实现分布式事务
我们的业务场景是Admin管理员记录日志、调用订单微服务发起一个订单、商品根据订单的数量减少相应的商品
管理员的日志、发起订单、货物减少 这是3个系统的交互,我们要保证分布式事务的一致性。
所以我们有3个微服务,Admin记录操作、Order记录订单、Goods删减货物
1.2PC的原理
1.1 2PC概念
两阶段提交又称2PC,两个阶段:第一阶段:投票阶段 和第二阶段:提交/执行阶段。
举例 订单服务A,需要调用 支付服务B 去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理为失败状态。
1.2 2PC第一阶段
第一阶段分3步
- 事务询问
协调者 向所有的 参与者 发送事务预处理请求,称之为Prepare,并开始等待各 参与者 的响应 - 协调者
各个 参与者 节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向 协调者 报告说:“我准备好了/我不行”。 - 各参与者向协调者反馈事务询问的响应
如果 参与者 成功执行了事务操作,那么就反馈给协调者 Yes 响应,表示事务可以执行
如果没有 参与者 成功执行事务,那么就反馈给协调者 No 响应,表示事务不可以执行
第一阶段执行完后,会有两种可能。1、所有都返回Yes. 2、有一个或者多个返回No。
1.3 2PC第二阶段
第二阶段分2步
- 所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交
协调者 向 所有参与者 节点发出Commit请求 - 事务提交
参与者 收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源
1.4 2PC异常处理
异常处理也分为2步
- 发送回滚请求
协调者向所有参与者节点发出 RoollBack 请求 - 事务回滚
参与者 接收到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事务处理流程
具体的执行流程如下:
- 用户服务的 TM 向 TC 申请开启一个全局事务,注意只有用户TM、没有积分TM,因为TM事务管理者只有一个发起者,发起全局事务的开启,全局事务创建成功并生成一个全局唯一的XID。
- 用户服务的 RM 向 TC 注册 分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局事务的管辖。
- 用户服务执行分支事务,向用户表插入一条记录,注意此处事务已经提交、资源得到释放,此处解决2pc性能问题。
- 逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的RM 向 TC 注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。
- 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务,注意此处事务也同样提交,资源得到释放,解决2pc性能问题。
- 用户服务分支事务执行完毕。
- TM 向 TC 发起针对 XID 的全局提交或回滚决议,提交的事务回滚其实就是对应的做减法操作,增加记录对应删减记录。
- 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下面的配置,填写自己的地址、数据库、及账号密码
!!!! 有两个地方注意下
- 我本地装的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的事务管理发起者
- 管理员发起事务、记录日志、调用订单微服务的RPC接口,进行下单
- 订单微服务的RPC接口下单,处理下单业务,Order微服务在下单完成后
- 货物微服务根据下单数量,去库存中减少货物数量,如果库存不够,下单失败,所有操作全部回滚
所以这三步是一个流程,管理员操作日志没写入、或者订单下单失败、或者货物存储不够,全部要回滚操作,这才是一个完整的事务
下面我们看下Admin 管理员中的操作
分析一下代码
- 方法上加上全局事务开启 @GlobalTransactional
- 第一步 管理员开启日志
- 第二部 掉订单微服务RPC接口,下订单
- 第三步 掉货物微服务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管理员日志
分析:
- 开启全局事务 Begin new global transaction [169.254.177.158:8091:128635569157300224]
- 全局事务ID Admin 开始全局事务,XID = 169.254.177.158:8091:128635569157300224
- 最终状态时第二阶段PhaseTwo 提交 Branch commit result: PhaseTwo_Committed
Order订单微服务日志
分析:
- 有一个全局事务的XID Order 全局事务,XID = 169.254.177.158:8091:128635569157300224
- 通过XID,本地事务有个BranchID rm client handle branch commit process:xid=169.254.177.158:8091:128635569157300224,branchId=128635572042981377
- 第二阶段成功提交 Branch commit result: PhaseTwo_Committed
Goods货物微服务日志
分析:
- 有一个全局事务的XID Order 全局事务, Goods 全局事务,XID = 169.254.177.158:8091:128635569157300224
- 通过XID,本地事务有个BranchID Branch committing: 169.254.177.158:8091:128635569157300224 128635573733285889
- 第二阶段成功提交 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的分布式事务 就完毕了,还是好用的,对代码入侵较少,比较优雅