一,Seata Server - TC全局事务协调器
Seata AT 事务原理 《分布式事务(三)Seata分布式事务框架-AT模式介绍》,介绍了 AT 事务的三个角色:TC(事务协调器)、TM(事务管理器)和RM(资源管理器),其中 TM 和 RM 是嵌入在业务应用中的,而 TC 则是一个独立服务。
Seata Server 就是 TC,直接从官方仓库下载启动即可,下载地址:
官方地址
二,Seata Server 配置
Seata Server 的配置文件有两个:
- seata/conf/registry.conf – 向注册中心注册
- seata/conf/file.conf – 协调器运行过程中记录的日志数据,要存到数据库
- seata/bin/seata-server.bat – 使用的内存默认2G,测试环境把内存改小:256m
1.registry.conf
Seata Server 要向注册中心进行注册,这样,其他服务就可以通过注册中心去发现 Seata Server,与 Seata Server 进行通信。
Seata 支持多款注册中心服务:nacos 、eureka、redis、zk、consul、etcd3、sofa。
Seata Server 要向注册中心进行注册,这样,其他服务就可以通过注册中心去发现 Seata Server,与 Seata Server 进行通信。
Seata 支持多款注册中心服务:nacos 、eureka、redis、zk、consul、etcd3、sofa。
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 {
......
}
......
2.file.conf
Seata 需要存储全局事务信息、分支事务信息、全局锁信息,这些数据存储到什么位置?
针对存储位置的配置,支持放在配置中心,或者也可以放在本地文件。Seata Server 支持的配置中心服务有:nacos 、apollo、zk、consul、etcd3。
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 {
......
}
}
3.seata-server.bat
三,启动 Seata Server
双击 seata-server.bat 启动 Seata Server。
注意:这里jdk文件只能是1.8的版本
查看 Eureka 注册中心 Seata Server 的注册信息:
注意:
这里不能选中,否则会连接超时
右键取消选中
四,在父项目order-parent添加seata依赖
<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>
五,订单模块(order)添加Seata AT事务
订单调用库存和账户,我们先从前面的订单开始。
在订单项目中要启动全局事务,还要执行订单保存的分支事务。
1.配置application.yml
spring:
application:
name: order
datasource:
url: jdbc:mysql:///seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
jdbcUrl: ${spring.datasource.url}
cloud:
alibaba:
seata:
tx-service-group: order_tx_group #当前模块在哪个事务组中
# account 8081 storage 8082 order 8083
server:
port: 8083
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.drhj.order.entity
configuration:
map-underscore-to-camel-case: true #驼峰命名
logging: #打印日志
level:
com.drhj.order.mapper: debug
ribbon:
MaxAutoRetriesNextServer: 0 #关闭重试
2.配置registry.conf
需要从注册中心获得 TC 的地址,这里配置注册中心的地址。
TC 在注册中心注册的服务ID在下面 file.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"
}
}
3.file.conf
在这里我们指定 TC 的服务ID seata-server:
vgroupMapping.order_tx_group = “seata-server”
order_tx_group 对应 application.yml 中注册的事务组名。
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
}
}
4.创建 seata 数据源代理
Seata AT 事务对业务代码无侵入,全自动化处理全局事务,其功能是靠 Seata 的数据源代理工具实现的。
这里我们创建 Seata 的数据源代理,并排除 Spring 默认的数据源。
package com.drhj.order;
import com.zaxxer.hikari.HikariDataSource;
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 DSPAutoConfiguration {
//新建原始数据源对象
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
//新建数据源代理对象
@Primary //首选对象,返回值相同
@Bean
public DataSource dataSourceProxy(DataSource ds) {
return new DataSourceProxy(ds);
}
}
主程序中排除Springboot 的默认数据源:
package com.drhj.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@MapperScan("com.drhj.order.mapper")
//禁用spring默认的数据源配置
//只使用自定义配置 DSPAutoConfiguration
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
5.启动全局事务
Seata AT 对业务无侵入,所以启动全局事务非常简单,只需要添加一个 @GlobalTransactional 注解即可。
另外我们一步一步地添加全局事务并测试,这里先把 storage 和 account 调用注掉。
package com.drhj.order.service;
import com.drhj.order.entity.Order;
import com.drhj.order.feign.AccountClient;
import com.drhj.order.feign.EasyIdClient;
import com.drhj.order.feign.StorageClient;
import com.drhj.order.mapper.OrderMapper;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private EasyIdClient easyIdClient;
@Autowired
private AccountClient accountClient;
@Autowired
private StorageClient storageClient;
@GlobalTransactional //启动全局事务,只在第一个模块上添加
@Transactional
@Override
public void create(Order order) {
//远程调用发号器,生成订单id
String s = easyIdClient.nextId("order_business");
Long id = Long.valueOf(s);
order.setId(id);
orderMapper.create(order);
//远程调用库存,减少库存
//storageClient.decrease(order.getProductId(),order.getCount());
//远程调用账户,扣减账户
//accountClient.decrease(order.getUserId(),order.getMoney());
}
}
6.启动 order 项目进行测试
一次启动 Eureka --> Seata Server --> Easy Id Generator --> Order
访问地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
观察控制台,看到全局事务和订单的分支事务已经启动,并可以看到全局事务ID(XID)和分支事务ID(Branch ID):
然后观察数据库中新添加的订单数据:
7.accout与storage配置同上
唯一的不同的是业务实现类不需要加全局事务
六,全局测试
1.成功测试
1)启动db_init 刷新数据库
2)依次启动 Eureka --> Seata Server --> Easy Id Generator --> account --> storage --> Order
3)访问地址:http://localhost:8083/create?userId=1&productId=1&count=10&money=100
订单会调用库存和账户,这三个服务会分别启动一个分支事务,三个分支事务一起组成一个全局事务:
观察三个项目的控制台都有Seata AT事务的日志及数据库信息
order:
account:
storage:
2.失败测试
1)模拟账户异常
2)重启账户服务
3)访问地址:http://localhost:8083/create?userId=1&productId=1&count=10&money=100
4)观察三个项目的控制台都有Seata AT事务的日志及数据库信息
order:
account:
storage:
可见,数据库实现了回滚。