SpringCloud事务之AT事务
1Seata-Server-TC全局事务协调器
Seata Server - TC全局事务协调器
AT 事务的三个角色:TC(事务协调器)、TM(事务管理器)和RM(资源管理器),其中 TM 和 RM 是嵌入在业务应用中的,而 TC 则是一个独立服务。
Seata Server 就是 TC,直接从官方仓库下载启动即可,下载地址:https://github.com/seata/seata/releases
Seata Server 配置
Seata Server 的配置文件有两个:
- seata/conf/registry.conf
- seata/conf/file.conf
Seata Server 要向注册中心进行注册,这样,其他服务就可以通过注册中心去发现 Seata Server,与 Seata Server 进行通信。
Seata 支持多款注册中心服务:nacos 、eureka、redis、zk、consul、etcd3、sofa。
我们项目中要使用 eureka 注册中心,eureka服务的连接地址、注册的服务名,这需要在 registry.conf 文件中进行配置:
registry.conf文件
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# 这里选择 eureka 注册配置
type = "eureka"
nacos {
......
}
# eureka的注册配置
eureka {
# 注册中心地址
serviceUrl = "http://localhost:8761/eureka"
# 注册的服务ID
application = "seata-server"
weight = "1"
}
redis {
......
}
......
file.conf
Seata 需要存储全局事务信息、分支事务信息、全局锁信息,这些数据存储到什么位置?
针对存储位置的配置,支持放在配置中心,或者也可以放在本地文件。Seata Server 支持的配置中心服务有:nacos 、apollo、zk、consul、etcd3。
这里我们选择最简单的,使用本地文件,这需要在 registry.conf 配置文件中来指定
file.conf
中对事务信息的存储位置进行配置,存储位置支持:file、db、redis。
这里我们选择数据库作为存储位置,这需要在 file.conf
中进行配置:
store {
## store mode: file、db、redis
# 这里选择数据库存储
mode = "db"
## file store property
file {
......
}
# 数据库存储
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.jdbc.Driver"
# 数据库连接配置
url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"
user = "root"
password = "root"
minConn = 5
maxConn = 30
# 事务日志表表名设置
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
......
}
}
收件将seata-server-1.3.0.zip文件下载下来,放到一个不含中文的文件目录中,进行解压
2启动参数设置
进入bin目录,找到seata-server.bat文件使用文本文件打开,找到85行,
将内存的配置(目的改小内存配置,节省内存)
%JAVACMD% %JAVA_OPTS% -server -Xmx2048m -Xms2048m -Xmn1024m -Xss512k -XX:Sur......
改为
%JAVACMD% %JAVA_OPTS% -server -Xmx256m -Xms256m -Xmn128m -Xss512k -XX:Sur......
本次项目的存储使用的数据进行存储,所以要修改conf中的file.conf中的文件中的存储内容
修改conf目录下的file.conf中的文件
修改conf目录下registry.conf文件
启动 Seata Server
首先启动Eureka注册中心
进入Seata Server的bin目录,双击 seata-server.bat
启动 Seata Server。
查看 Eureka 注册中心 Seata Server 的注册信息:
order订单服务添加 Seata AT 事务
订单调用库存和账户,我们先从前面的订单开始。
在订单项目中要启动全局事务,还要执行订单保存的分支事务。
3添加seata依赖
order-parent 添加 seata 依赖
order-parent 的 pom.xml 文件中有一段注释掉的 seata 依赖,现在可以打开它了
order-parent 的 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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>order-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>order-parent</name>
<description>Demo project for Spring Boot</description>
<properties>
<mybatis-plus.version>3.3.2</mybatis-plus.version>
<druid-spring-boot-starter.version>1.1.23</druid-spring-boot-starter.version>
<seata.version>1.3.0</seata.version>
<spring-cloud-alibaba-seata.version>2.0.0.RELEASE</spring-cloud-alibaba-seata.version>
<spring-cloud.version>Hoxton.SR6</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>${spring-cloud-alibaba-seata.version}</version>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>${seata.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<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>
4配置order模块
配置order中的application.yml文件,TC 事务协调器通过“事务组”的方式将多个服务组织成一个全局事务。每个服务启动时要向TC注册,加入同一个事务组。
在order模块中要加入事务组
spring:
......
cloud:
alibaba:
seata:
tx-service-group: order_tx_group
......
需要从注册中心获得 TC 的地址,这里配置注册中心的地址。
registry.conf文件内容
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "eureka"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
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"
session.timeout = 6000
connect.timeout = 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、springCloudConfig
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
file.conf中的配置
在这里我们指定 TC 的服务ID seata-server
:
vgroupMapping.order_tx_group = “seata-server”
order_tx_group
对应 application.yml
中注册的事务组名。
# 事务组对应使用哪个协调器
# 事务组组名=协调器服务名
vgroupMapping.order_tx_group = "seata-server"
file.conf文件内容
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
# order_tx_group 与 yml 中的 “tx-service-group: order_tx_group” 配置一致
# “seata-server” 与 TC 服务器的注册名一致
# 从eureka获取seata-server的地址,再向seata-server注册自己,设置group
# 事务组对应使用哪个协调器
# 事务组组名=协调器服务名
vgroupMapping.order_tx_group = "seata-server"
#only support when registry.type=file, please don't set multiple addresses
order_tx_group.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
文件存放处
5创建seata数据源代理
Seata AT 事务对业务代码无侵入,全自动化处理全局事务,其功能是靠 Seata 的数据源代理工具实现的。
这里我们创建 Seata 的数据源代理,并排除 Spring 默认的数据源。
package cn.tedu.order;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
@Configuration
public class DatasourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSource(DataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
}
主程序中排除Springboot 的默认数据源:
在主程序上添加以下注解
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
因为seat的属于源配置url与datasource不一致,所以,要在application.yml中添加一个新的数据源连接
jdbcUrl: jdbc:mysql://localhost:3307/seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
6启动全局事务
Seata AT 对业务无侵入,所以启动全局事务非常简单,只需要添加一个 @GlobalTransactional
注解即可。
另外我们一步一步地添加全局事务并测试,这里先把 storage 和 account 调用注掉。
进入order模块中的cn.tedu.order.servie包,找到OrderServiceImpl实现类,给create方法添加全局事务,
@GlobalTransactional
7启动测试
按顺序启动服务:
1.Eureka
2.Seata Server
3.Easy Id Generator
4.Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
观察控制台,看到全局事务和订单的分支事务已经启动,并可以看到全局事务ID(XID)和分支事务ID(Branch ID):
然后观察数据库中新添加的订单数据:
8模拟回滚
测试出现异常,回滚的情况
在OrderServiceImpl业务代码中加一个模拟异常再试一下:
throw new RuntimeException("模拟异常");
重启 order 项目,并调用保存订单:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
可以看到全局事务回滚的日志:
测试结束以后就可以把模拟异常给注释掉
9给storage模块添加SeataAT事务
首先给storage的application.yml文件配置事务组
#事务组的组名
cloud:
alibaba:
seata:
tx-service-group: order_tx_group
将order模块中resiurces文件夹中的file.conf和registry.conf文件拷贝到storage模块中的resources文件夹下。
创建seata数据源代理
将order模块中cn.tedu.order包下的DSAutoConfiguration拷贝到storage模块下的cn.tedu.storage包下
在主程序注解排除DataSourceAutoConfiguration
自动配置类
在主启动类上添加
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
在application.yml配置文件中新加一个seat的数据源配置
jdbcUrl: jdbc:mysql://localhost:3307/seata_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
启动分支事务
在业务方法上添加 @Transactional
注解启动本地事务:
在storage项目下cn.tedu.storage.service包中的StorageServiceImpl实现类下的decrease方法添加@Transactional注解
package cn.tedu.storage.service;
import cn.tedu.storage.mapper.StorageMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class StorageServiceImpl implements StorageService{
@Autowired
private StorageMapper storageMapper;
@Transactional
@Override
public void decrease(Long productId, Integer count) {
storageMapper.decrease(productId,count);
}
}
在order的OrderServiceImpl业务代码中把调用商品减少的库存
前面我们把调用商品库存注释掉了,现把注释打开:
10启动storage项目
按顺序启动项目:
1.Eureka
2.Seata Server
3.Easy Id Generator
4.Storage
5.Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
order数据库新增一条数据
storage数据库中数据减少
订单会调用库存,这两个服务会分别启动一个分支事务,两个分支事务一起组成一个全局事务:
观察两个项目的控制台都有Seata AT事务的日志,Storage 项目控制台如下:
order模块的控制台
11模拟异常
在storage模块在业务代码中加一个模拟异常再试一下:
throw new RuntimeException("模拟异常");
重启 storage 项目,并调用保存订单:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
Order模块进行了回滚
storage模块出现了异常
可以得出结论,数据回滚了
storage模块完成测试以后,要把注释给注释去掉
11account模块添加SeataAT事务
首先在account模块中的appplication.yml添加事务组,要在spring的配置下添加
#事务组的组名
cloud:
alibaba:
seata:
tx-service-group: order_tx_group
将order项目中resources文件夹中的file.conf,registry.conf文件复制account同样的文件夹中
创建seata数据源代理
与订单项目和库存项目中数据源代理相同,将order模块cn.tedu.order包中的DSAutoConfiguration复制到account的cn.tedu.account中
在主启动类中排除掉DataSourceAutoConfiguration配置,在主启动类添加以下注解
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
由于hikari 的数据库url属性名是 jdbcUrl,不是url所以需要在配置文件中添加一个jdbcUrl的配置
jdbcUrl: jdbc:mysql://localhost:3307/seata_account?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8
启动分支事务
在AccoutServiceImpl业务上添加本地事务的注解
在order的业务类中调用扣减账户金额
前面我们把调用账户注释掉了,现把注释打开:
12启动account项目进行测试
按顺序启动项目:
Eureka
Seata Server
Easy Id Generator
Storage
Account
Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
订单会调用库存和账户,这三个服务会分别启动一个分支事务,三个分支事务一起组成一个全局事务:
观察三个项目的控制台都有Seata AT事务的日志,account 项目控制台如下:
order控制台
Storage控制台
Account控制台
然后观察数据库中的订单表、库存表和账户表。
order表
storage表
account表
13测试Account异常
在Account的业务类AccoutServiceImpl添加异常信息
throw new RuntimeException("模拟异常");
重启 account 项目,并调用保存订单:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
查看数据库表 order、storage 和 account,如果执行成功会新增订单、减少库存、扣减金额,如果执行失败则数据没有变化,被回滚了。
失败时,在 order 和 storage和account 控制台可以看到回滚日志。
order控制台
stotage控制台
account控制台
查看数据库表
account表
order表
stotage表
完成以后把account的异常注释去掉
SeataAT的事务完成