分布式事务(二)Seata AT 模式
Seata AT 事务方案
Seata 的 AT 模式(Automatic Transaction)是一种无侵入的分布式事务解决方案。
业务场景:订单系统
当用户下订单时,执行以下三步流程:
- 订单系统保存订单
- 订单系统调用库存服务,减少商品库存
- 订单系统调用账户服务,扣减用户金额
这三步要作为一个整体事务进行管理,要么整体成功,要么整体失败。
Seata AT 基本原理
Seata AT 事务分两个阶段来管理全局事务:
第一阶段: 执行各分支事务
微服务系统中,各服务之间无法相互感知事务是否执行成功,这时有一个专门的服务,来协调各个服务的运行状态。这个服务称为 TC(Transaction Coordinator),事务协调器。
- 订单系统开始执行保存订单之前,首先启动 TM(Transaction Manager,事务管理器),由 TM 向 TC 申请开启一个全局事务。
- 这时TC会产生一个全局事务ID,称为 XID,并将 XID 传回 TM,这样就开启了 全局事务。
- 全局事务开启后,开始执行创建订单的业务。首先执行保存订单,这时会先启动一个 RM(Resource Manager,资源管理器),并将 XID 传递给 RM。
- RM 负责对分支事务(即微服务的本地事务)进行管理,并与 TC 通信,上报分支事务的执行状态、接收全局事务的提交或回滚指令。
- RM 首先会使用 XID 向 TC 注册分支事务,将分支事务纳入对应的全局事务管辖。
- 现在可以执行保存订单的分支事务了。一旦分支事务执行成功,RM 会上报事务状态。
- TC 收到后,会将该状态信息传递到 TM,到此,保存订单过程结束。
- 下面是调用库存服务,减少商品库存,与订单的执行过程相同。
首先调用库存服务,启动 RM,并传递 XID。 - 库存服务的 RM 使用 XID 向 TC 进行注册,纳入全局事务管辖。
- 执行本地事务成功后上报状态,TC会将状态发送给TM。
- 相同的,完成账户分支事务。
第二阶段: 控制全局事务最终提交或回滚
- 现在,TM(全局事务管理器)收集齐了全部分支事务的成功状态,它会进行决策,确定全局事务成功,向 TC 发送全局事务的提交请求。
- 然后,TC 会向所有 RM 发送提交操作指令,RM 会完成最终提交操作。
- 到此,全局事务全部提交完成。
第二阶段:控制全局事务最终回滚
- 上面是全局事务执行成功的情况,下面再来看看事务执行失败的情况。
假设订单业务执行过程中,扣减账户金额这一步分支事务执行失败,那么失败状态对TC上报,然后再发送给TM。 - TM 会进行决策,确定全局事务失败,向 TC 发送全局事务的回滚请求。
- 然后,TC 会向所有 RM 发送回滚操作指令,RM 会完成最终回滚操作。
Seata Server - TC 全局事务协调器
AT 事务的三个角色:
TC(事务协调器)、TM(事务管理器)和RM(资源管理器)。
其中 TM 和 RM 是嵌入在业务应用中的,而 TC 则是一个独立服务。
启动事务协调器(TC)-- seata server
解压 “课前资料/分布式事务/seata-server-1.3.zip”
更改三个配置文件
-
registry.conf – 向注册中心注册(conf 目录下)
更改注册方式:eureka
更改 eureka 应用名:seata-server
-
file.conf – seata 保存日志(conf 目录下)
更改存储模式:db
更改 db 配置:驱动改为 cj 包、用户名密码改为自己的、url 参数添加时区。
-
seata-server.bat 内存配置(bin目录下)
更改 85 行 Xmx/Xms/Xmn 参数分别为 256/256/128
执行 seata-server.bat 启动,eureka 注册中心有 SEATA-SERVER 服务名即可。
- 若无法启动,JAVA_HOME 和 PATH的配置
- 若无法启动,若新版本 JDK,有的启动参数不支持,按照报错删掉这些参数即可。
- 在 cmd 中不能选中或指示白点,否则程序会变为挂起状态,无法使用。
添加 Seata AT 事务
在业务模块中,添加 Seata AT 事务
Order 订单模块
添加 seata 依赖:
-
把 order-parent 的 pom 文件中的注释部分打开。
-
重启 三个业务模块,会发现启动不了:
NotSupportYetException: config type can not be null 报错。
原因是没有添加配置,属于正常情况。
修改 order 订单服务三个配置文件
-
application.yml – 事务组命名
添加 cloud.alibaba.seata.tx-service-group 配置,命名为 order_tx_group。
spring:
cloud:
alibaba:
seata:
# 三个模块 order,storage,account 都要在这同一个事务组中
tx-service-group: order_tx_group
-
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 – 事务组对应使用哪个协调器
从笔记复制即可。查看 36 行配置,当前事务组,对应使用哪个协调器。
事务组命名要与 file.conf 中 36 行的参数一致。
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
}
}
创建数据源代理
-
在 order 包下创建自动配置类 DSAutoConfiguration:
在类上添加配置类注释 @Configuration。
-
创建原始数据源对象(通过方法):
创建 DataSource 数据源对象方法 dataSource,返回一个 Hikari 数据源对象,方法上添加 @Bean 交给 Spring 容器管理,添加 @ConfigurationProperties 注解并指定前缀,这里的前缀是指 datasource 在 application.yml 中的具体路径(spring.datasource)。(这里也可以返回一个阿里的数据源对象,DruidDataSource)
@Configuration
public class DSAutoConfiguration {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource dataSource(){
return new HikariDataSource();
}
}
注意这里的引包,DataSource 是要引 javax.sql 包。
-
修改 application.yml 中 datasource 属性:
因为 Hikari 需要一个 jdbcUrl 的属性,所以我们添加它,值和上面的 url 一致。
jdbcUrl: ${spring.datasource.url}
-
创建数据源代理对象:
创建一个方法 dataSourceProxy,参数为数据源对象,返回一个数据源代理对象,方法上添加 @Bean 交给 Spring 容器注入。添加 @Primary 首选项
@Primary
@Bean
public DataSource dataSourceProxy(DataSource ds){
return new DataSourceProxy(ds);
}
-
启动类注解添加配置
在 @SpringBootApplication 注解后面添加 exclude = DataSourceAutoConfiguration.class
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
在 OrderServiceImpl 业务方法上,添加注解
- @GlobalTransactional – 用来启动全局事务,只在第一个模块添加
- @Transactonal – 控制本地事务
启动测试
-
为了进行测试,把业务方法类中的远程调用业务先注释
-
按顺序启动服务:
Eureka
Seata Server
Easy Id Generator
Order -
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100 -
观察控制台,看到全局事务和订单的分支事务已经启动,并可以看到全局事务ID(XID)和分支事务ID(Branch ID)
Account 和 Storage 业务模块
修改 storage 和 account 业务相关配置
- 修改 application.yml 文件,添加事务组相关配置(jdbcUrl 也包括)。
- 将 order 订单服务的两个 conf 配置文件直接复制到业务目录下。
- 若 conf 配置文件未生效,可 build、compile、重启解决。
创建 seata 数据源代理
和 order 订单服务完全相同,可直接复制 DSAutoConfiguration。
排除 Spring 默认配置
在启动类注解添加参数 (exclude = DataSourceAutoConfiguration.class)。
在业务方法上添加注解
ServiceImpl 实现类方法上添加 @Transactional 注解,控制本地事务。
将 OrderServiceImpl 实现类方法上注释的部分解开。
启动 storage 项目进行测试
按顺序启动项目:
- Eureka
- Seata Server
- Easy Id Generator
- Storage
- Account
- Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
订单会调用库存和账户,这三个服务会分别启动一个分支事务,三个分支事务一起组成一个全局事务。
观察三个项目的控制台都有 Seata AT 事务的日志即可。
然后观察数据库中的订单表、库存表和账户表。
测试出现异常回滚
调整 Account 类,使得金额不足时出错。
Mapper 添加查询方法 selectByUserId 返回一个 Account 对象,参数为 userId。
Mapper.xml 添加 SQL 语句:
<select id="selectByUserId" resultMap = "BaseResultMap">
select * from account where user_id = #{userId};
</select>
在 ServiceImpl 实现类中 decrease 方法中增加逻辑:
判断账户可用余额 residue 是否小于金额 money,若小于则抛出异常 “可用金额不足!”
public void decrease(Long userId, BigDecimal money) {
Account account = accountMapper.selectByUserId(userId);
/*
BigDecimal a BigDecimal b
a.compareTo(b)
返回值
正数,表示a大
负数,表示a小
0,表示相同
*/
if (account.getResidue().compareTo(money) < 0){
throw new RuntimeException("可用金额不足!");
}
accountMapper.decrease(userId,money);
}
重启测试
访问:
http://localhost:8083/create?userId=1&productId=1&count=10&money=400
查看数据库表 order、storage 和 account,如果执行成功会新增订单、减少库存、扣减金额,如果执行失败则数据没有变化,被回滚了。
失败时,在 order 和 storage 控制台可以看到回滚日志。
storage 控制台:
account 控制台:
order 控制台: