SpringCloud - Seata 分布式事务
前言
该博客为Sentinel
学习笔记,主要目的是为了帮助后期快速复习使用
学习视频:7小快速通关SpringCloud
辅助文档:SpringCloud快速通关
源码地址:cloud-demo
一、简介
官网:https://seata.apache.org/zh-cn/
Seata
是一款开源的分布式事务解决方案,旨在解决微服务架构中跨服务的事务一致性问题。它支持多种事务模型(如AT、TCC、SAGA),能够保证跨多个服务和数据库的数据一致性,简化了分布式事务的管理。
为什么需要使用 Seata?
在分布式系统中,由于涉及多个微服务和多个数据库,事务的一致性和可靠性成为了一个挑战。传统的事务管理机制(如两阶段提交)不适用于复杂的微服务环境,因为它们的成本高、实现复杂且不易扩展。
Seata 的优势体现在以下几个方面:
- 分布式事务支持:
Seata
提供了跨多个服务和数据库的事务支持,确保多个服务的操作能够像一个单一事务一样进行提交或回滚。 - 性能优化:
Seata
采用高效的事务协调机制,通过减少分布式事务处理的开销,显著提升系统性能。 - 可靠性和容错性:
Seata
的设计保证了事务的一致性和可靠性,即使在部分节点发生故障时,也能保证系统的正确性。 - 简化开发:使用
Seata
时,开发者不需要自己编写复杂的分布式事务处理逻辑,Seata 提供了易用的注解和API,极大地简化了开发工作。 - 灵活的事务模型:
Seata
提供了多种事务模型(AT
、TCC
、SAGA
),可以根据具体业务场景选择合适的事务管理方式,满足不同的需求。
二、快速入门
2.1 环境准备
2.2 原理
2.2.1 核心组件
Seata
分布式事务的工作原理主要涉及三个核心组件:事务协调器(TC
)、事务管理器(TM
)和资源管理器(RM
)。以下是这些组件的详细工作原理:
- 事务协调器(TC):
TC
负责维护全局事务的状态,并协调事务的提交或回滚。 它接收全局事务的开始、提交或回滚请求,协调各个服务的事务执行。TC在收到所有分支事务的提交或回滚确认后,将全局事务状态标记为结束。 - 事务管理器(TM):
TM
负责定义和发起全局事务。 当TM发起一个全局事务时,TC创建一个全局事务ID,并将该事务状态设置为“开始”状态。TM通知TC提交事务,TC依次通知各RM提交其分支事务。如果任一分支事务执行失败或出现异常,TM通知TC回滚事务,TC会依次通知各RM进行回滚操作。 - 资源管理器(RM):
RM
负责管理分支事务(Branch Transaction),并与本地数据库资源交互,确保各个分支事务的提交和回滚操作。 在全局事务的生命周期内,多个服务会分别执行属于自己的本地事务(即分支事务)。每个分支事务在执行前需要向TC注册,并绑定到全局事务ID上。每个服务的RM负责管理分支事务的执行,并与本地资源交互。执行完成后,RM会向TC报告分支事务的执行结果。
2.2.1 实现步骤
Seata
的分布式事务管理通过以下步骤实现:
- 全局事务开始:当TM发起一个全局事务时,TC创建一个全局事务ID,并将该事务状态设置为“开始”状态。
- 注册分支事务:在全局事务的生命周期内,多个服务会分别执行属于自己的本地事务(即分支事务)。每个分支事务在执行前需要向TC注册,并绑定到全局事务ID上。
- 分支事务执行:每个服务的RM负责管理分支事务的执行,并与本地资源交互。执行完成后,RM会向TC报告分支事务的执行结果。
- 全局提交或回滚:当所有分支事务都执行成功时,TM通知TC提交事务,TC依次通知各RM提交其分支事务。如果任一分支事务执行失败或出现异常,TM通知TC回滚事务,TC会依次通知各RM进行回滚操作。
- 事务结束:TC在收到所有分支事务的提交或回滚确认后,将全局事务状态标记为结束。
2.3 seata-server
- 下载:https://seata.apache.org/zh-cn/download/seata-server
- 解压并启动:
seata-server.bat
Server端口【TC事务协调者】:8091
Web可视化界面:http://localhost:7091
,账号和密码都为seata
2.4 微服务配置
2.4.1 引依赖
<!-- 分布式事务Seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
2.4.2 配置
每个微服务创建 file.conf
文件,完整内容如下;
【微服务只需要复制 service 块配置即可】
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
transport {
# tcp, unix-domain-socket
type = "TCP"
#NIO, NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the tm client batch send request enable
enableTmClientBatchSendRequest = false
# the rm client batch send request enable
enableRmClientBatchSendRequest = true
# the rm client rpc request timeout
rpcRmRequestTimeout = 2000
# the tm client rpc request timeout
rpcTmRequestTimeout = 30000
# the rm client rpc request timeout
rpcRmRequestTimeout = 15000
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size
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
vgroupMapping.default_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.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
tableMetaCheckerInterval = 60000
reportSuccessEnable = false
sagaBranchRegisterEnable = false
sagaJsonParser = "fastjson"
sagaRetryPersistModeUpdate = false
sagaCompensatePersistModeUpdate = false
tccActionInterceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
sqlParserType = "druid"
branchExecutionTimeoutXA = 60000
connectionTwoPhaseHoldTimeoutXA = 10000
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
defaultGlobalTransactionTimeout = 60000
degradeCheck = false
degradeCheckPeriod = 2000
degradeCheckAllowTimes = 10
interceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
}
undo {
dataValidation = true
onlyCareUpdateColumns = true
logSerialization = "jackson"
logTable = "undo_log"
compress {
enable = true
# allow zip, gzip, deflater, lz4, bzip2, zstd default is zip
type = zip
# if rollback info size > threshold, then will be compress
# allow k m g t
threshold = 64k
}
}
loadBalance {
type = "XID"
virtualNodes = 10
}
}
log {
exceptionRate = 100
}
tcc {
fence {
# tcc fence log table name
logTableName = tcc_fence_log
# tcc fence log clean period
cleanPeriod = 1h
}
}
2.4.3 开启全局事务
在最大的方法入口,标记@GlobalTransactional
,即可开启全局事务
2.5 二阶提交协议原理
第一阶段:本地事务
- 全局事务开始:业务层发起全局事务请求,事务协调者(
TC
)注册全局事务并生成全局事务ID。 - 分支事务注册:各个分支事务(如扣减库存、创建订单、扣减余额)分别向
TC
注册,申请必要的资源锁(如数据库表的记录锁)。 - 执行本地事务:
- 扣减库存:扣减库存:执行SQL
update storage_tbl set count = count - 2 where commodity_code = 'P0001'
,更新库存数量。 - 创建订单:执行订单创建相关的本地事务。
- 扣减余额:执行扣减账户余额的本地事务。
- 扣减库存:扣减库存:执行SQL
- 记录前后镜像:在执行业务SQL前后,分别查询并记录数据的前后镜像,用于生成回滚日志(
UNDO LOG
)。 - 插入回滚日志:将前后镜像组成的回滚日志插入到
UNDO LOG
表中,以便在需要时进行数据回滚。 - 本地事务提交:将业务数据和
UNDO LOG
一起保存,并汇报自己提交成功与否的结果。
第二阶段:提交阶段
- 分支提交:
- 收到
TC
的提交请求后,各个分支事务立即响应OK
,表示准备就绪。 - 给异步任务队列中添加异步任务,异步和批量地删除相应的
UNDO LOG
记录。
- 收到
- 分支回滚:
- 如果TC的提交请求未能成功,或者在第一阶段中某个分支事务失败,
TC
将发起回滚请求。 - 各个分支事务收到回滚请求后,开启一个本地事务,执行以下任务:
- 找到对应的UNDO LOG记录。
- 数据校验:比较后镜像与当前数据,如果不一致,则说明数据被其他渠道修改,需要进行相应处理;如果一致,则执行回滚。
- 回滚数据:根据
UNDO LOG
的前镜像内容执行数据修改,完成后删除UNDO LOG
。
- 如果TC的提交请求未能成功,或者在第一阶段中某个分支事务失败,
通过以上流程,Seata
确保了分布式事务的原子性,即所有分支事务要么全部成功提交,要么全部回滚,从而保证了数据的一致性。
2.6 二阶提交协议可视化
2.6.1 观察初始数据库
2.6.2 设置断点
我们在业务服务BusinessServiceImpl
的主方法purchase
中给远程调用方法deduct
和create
加上断点
并在订单服务OrderServiceImpl
的create方法中添加一个除零异常,并在前面加上一个断点
2.6.3 发起请求,观察数据变化
在浏览器发送请求,http://localhost:11000/purchase?userId=1&commodityCode=P0001&count=2
这里,我们来到第一个断点扣减库存deduct
,可以看到控制台打印开启了一个新的全局事务
此时在Seata
控制台中可以观测到这个全局事务
当前没有任何数据提交,即没有全局锁
->
只有第一阶段本地提交,要修改数据之前,才会申请自己数据的记录锁,即全局锁
继续放行到第二个断点位置,创建订单create
,观察Seata
控制台,开启分支事务并查看
可以看到storage_db
的分支事务
而且在全局锁中也可以看到数据 -> 分支事务一定会提交一个数据
继续观察storage_db
数据库,可以看到库存已经被扣减,并且在undo_log
表中有一条记录
查看rollback_info
字段的内容,
在前镜像" beforeImage "
中可以看到,在记录修改之前的值为100 "value": 100
后镜像"afterImage"
,扣减库存后的数据为,"value": 98
{
"@class": "org.apache.seata.rm.datasource.undo.BranchUndoLog",
"xid": "192.168.4.105:8091:7782838703486791689",
"branchId": 7782838703486791690,
"sqlUndoLogs": [
"java.util.ArrayList",
[
{
"@class": "org.apache.seata.rm.datasource.undo.SQLUndoLog",
"sqlType": "UPDATE",
"tableName": "storage_tbl",
"beforeImage": {
"@class": "org.apache.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "storage_tbl",
"rows": [
"java.util.ArrayList",
[
{
"@class": "org.apache.seata.rm.datasource.sql.struct.Row",
"fields": [
"java.util.ArrayList",
[
{
"@class": "org.apache.seata.rm.datasource.sql.struct.Field",
"name": "count",
"keyType": "NULL",
"type": 4,
"value": 100
},
{
"@class": "org.apache.seata.rm.datasource.sql.struct.Field",
"name": "id",
"keyType": "PRIMARY_KEY",
"type": 4,
"value": 1
}
]
}
}
]
]
},
"afterImage": {
"@class": "org.apache.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "storage_tbl",
"rows": [
"java.util.ArrayList",
[
{
"@class": "org.apache.seata.rm.datasource.sql.struct.Row",
"fields": [
"java.util.ArrayList",
[
{
"@class": "org.apache.seata.rm.datasource.sql.struct.Field",
"name": "count",
"keyType": "NULL",
"type": 4,
"value": 98
},
{
"@class": "org.apache.seata.rm.datasource.sql.struct.Field",
"name": "id",
"keyType": "PRIMARY_KEY",
"type": 4,
"value": 1
}
]
}
}
]
]
}
}
]
]
}
继续放行到第三个断点位置,保存订单orderTblMapper.insert(orderTbl);
,观察Seata
控制台
可以看到多了account_db
的分支事务和全局锁
此时数据库的余额和库存均被扣减,并且生成了UNDO LOG
记录
此时继续放行,即会执行int i = 1/0;
抛出除零异常,所有数据全部按照前镜像回滚,UNDO LOG
记录
注意:事务事务超时时间为一分钟,调试过程中,一旦超时会报一下错误
may be has finished
->
事务可能完成
branch register failed
->
分支注册事务失败
2025-02-13T22:41:16.010+08:00 ERROR 19556 --- [seata-storage] [io-13000-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.transaction.TransactionSystemException: JDBC commit failed] with root cause
org.apache.seata.core.exception.RmTransactionException: branch register failed, xid: 192.168.4.105:8091:7782838703486791747, errMsg: TransactionException[Could not found global transaction xid = 192.168.4.105:8091:7782838703486791747, may be has finished.]
2.7 四种事务模式
2.7.1 AT模式
这是Seata
中最常用的一种分布式事务模式,它通过自动的预提交、回滚操作,实现分布式事务的透明化管理。AT
模式的核心在于自动处理数据库的事务操作,不需要对业务代码进行侵入性的修改。
2.7.2 TCC模式
适用于需要显式控制分布式事务的场景,确保所有参与者都遵循统一的规则。TCC
模式通过三阶段的提交(Try、Confirm、Cancel)来保证最终一致性。适用于夹杂非数据库操作的事务。
2.7.3 Saga模式
适用于复杂的业务流程,每个步骤都可以独立地进行业务逻辑处理。在Saga
模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
2.7.4 XA模式
适用于需要兼容传统数据库系统的场景,支持多种资源管理器。XA模式是强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入。
2.7.5 切换模式
添加application.yml
配置
seata:
data-source-proxy-mode: AT #默认为AT模式