Seata 1.4.2
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/
该版本是 1.4.2,后续版本都不需要专门启动TC服务,直接引入依赖即可
Seata 三大核心角色
1. TM 事务发起者:加了@GlobalTransactional 的服务,负责开启全局事务、生成 XID、通知 TC 提交/回滚。
2. RM 资源管理者:所有参与分布式事务的微服务,负责执行本地事务、上报分支状态、执行回滚。
3. TC 事务协调器:就是你启动的 Seata Server(独立服务,端口8091),全局事务的“总指挥”。
核心真理:所有分布式事务的协调、记录、回滚、提交,全部由 TC 完成。

Seata 的两个核心配置文件
file.conf
控制 Seata Server(TC) 的事务数据持久化方式,注:仅当registry.conf 中的 config配置来源为 file 时生效。即:registry.conf 中的优先级高,若 registry.conf 中 config 设为 nacos 则优先使用 Nacos 配置,设为 file 才会读取本地 file.conf 配置。
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
publicKey = ""
file {
dir = "sessionStore"
maxBranchSessionSize = 16384
maxGlobalSessionSize = 512
fileWriteBufferCacheSize = 16384
sessionReloadReadSize = 100
flushDiskMode = async
}
## database store property
db {
datasource = "druid"
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
user = "mysql"
password = "mysql"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
mode = "single"
single {
host = "127.0.0.1"
port = "6379"
}
password = ""
database = "0"
minConn = 1
maxConn = 10
maxTotal = 100
queryLimit = 100
}
}
核心参数:store.mode 只有三种:file / db / redis
- store.mode=file(默认、本地开发用)
- 无需数据库、无需建任何表
- 事务数据存在本地文件
- 重启 Seata 事务数据丢失
- 适合:本地测试
- store.mode=db(生产、集群用)
- 必须连接 MySQL
- 必须在 对应seata 库执行 3 张核心表:global_table、branch_table、lock_table
- 持久化事务数据,重启不丢
- 适合:上线、集群、正式环境
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "syc"
username = ""
password = ""
}
file {
name = "file.conf"
}
.......
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "DEFAULT_GROUP"
username = ""
password = ""
dataId = "seataServer.properties"
}
file {
name = "file.conf"
}
.......
}
Seata 去哪找、怎么被发现
分为两块,和业务微服务完全无关:
- registry 注册:Seata Server 是否注册到注册中心(Nacos/ZK)
- config 配置:Seata Server 去哪读取自身配置(type=file:读取本地的 file.conf 配置 、type=nacos:读取nacos 中对应的配置)
核心参数:type = file / nacos
如果 type=file
seata 不注册到任何注册中心
Seata 客户端(你的微服务)不通过 Nacos 发现 Seata 服务端
微服务客户端必须硬写死 Seata 地址:
#type=file
seata:
registry:
type: file
# 配置方式 = file
config:
type: file
tx-service-group: seata-demo
# 重点!file 模式必须写死 TC 地址
service:
grouplist:
default: 127.0.0.1:8091
#type=nacos
seata:
registry:
type: nacos
nacos:
# 通过nacos找对应 TC服务
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-tc-server
username: nacos
password: nacos
tx-service-group: seata-demo
核心原理(跨服务检测)
常用场景举例:订单服务(TM)调用库存服务(RM)、支付服务(RM)
第一步:TM 开启全局事务,向TC申请唯一XID
接口添加 @GlobalTransactional 后,全局事务自动开启:
- 订单服务(TM)向 TC 发起请求,生成全局唯一 XID
- TC 记录事务初始化状态,XID 绑定当前线程上下文,贯穿所有服务
- 订单服务(TM角色)主动向 TC(127.0.0.1:8091) 发起TCP请求
- TC 生成一个全局唯一的XID(全局事务唯一标识,贯穿所有服务)
- TC 写入一条「全局事务初始化记录」(store.mode=file存文件,store.mode=db存global_table表)
- XID 绑定当前全局事务,状态标记为:进行中
- TM 拿到XID,绑定到当前线程上下文,全程携带
第二步:跨服务调用,XID自动透传(事务关联核心)
订单服务通过Feign调用下游服务时,Seata 拦截器自动完成 XID 透传:
- 自动将线程中的 XID 放入请求头,传递给下游
- 库存、支付服务自动提取 XID,纳入同一全局事务,成为分支 RM
核心关联逻辑:所有微服务共享同一个 XID,TC 以此判定所有服务属于同一个全局事务。
- 拦截Feign请求,从当前线程上下文取出全局XID
- 将XID放入HTTP请求请求头(Seata自定义请求头)
- 携带XID调用下游微服务,无需手动传参
下游所有被调用的服务(库存、支付),接收请求后:
第三步:所有RM执行本地事务,同步上报TC
所有下游 RM 执行本地业务事务,遵循 AT 模式核心流程:
- 执行SQL前生成 undo_log 回滚日志,留存数据快照
- 执行并提交本地事务,同时直连 TC 上报分支执行状态
- TC 记录分支信息与数据全局锁,防止并发脏写
整个事务上报、锁记录过程仅为 RM 与 TC 直连交互
- 执行本地SQL前,先查询数据库快照,生成undo_log回滚日志(存自身业务库)
- 执行本地业务事务,提交本地SQL
- 向TC上报:当前分支事务执行成功,TC在branch_table记录分支状态
- 同时TC在lock_table记录数据全局锁,防止其他事务脏写冲突
整个过程:所有RM(多个微服务)直接TCP直连TC8091端口,和Nacos无任何交互,Nacos仅负责Feign的服务地址发现。
第四步:TM收尾,TC统一全局决策(提交/回滚)
所有分支事务执行完毕后,由 TM 统一收尾,TC 全局决策:
- 无异常:TM 通知 TC 全局提交,所有 RM 清理本地 undo_log,事务正常结束
- 任意服务异常:TM 触发全局回滚,TC 通知所有 RM,通过 undo_log 还原数据
最终实现所有服务数据要么全部成功、要么全部回滚,保证分布式数据一致性。
- 无异常场景:TM向TC发送「全局提交」指令,TC通知所有RM,删除各自undo_log日志,全局事务结束,数据永久生效
- 任意服务异常/超时:只要有一个分支报错、抛出异常,TM感知后向TC发送「全局回滚」指令
TC收到回滚指令后,根据XID查询所有关联的分支事务,批量通知所有RM执行回滚:
搭建TC服务
- 首先我们要下载seata-server包,地址在http://seata.io/zh-cn/blog/download.html
- 解压修改配置:修改conf目录下的registry.conf文件:
- 修改配置文件,注册到nacos
registry {
# tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
#
type = "nacos"
nacos {
# seata tc 服务注册到 nacos的服务名称,可以自定义
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
# 命名空间
namespace = ""
# 集群名称
cluster = "syc"
username = ""
password = ""
}
}
config {
# 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置
type = "nacos"
# ---------------------------------------------------------------
# Seata 启动时会优先去 Nacos 配置中心 拉取配置(seataServer.properties),而不是读取本地的 .conf 文件中的配置
# ---------------------------------------------------------------
# 配置nacos地址等信息
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
consul {
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
apollo {
appId = "seata-server"
## apolloConfigService will cover apolloMeta
apolloMeta = "http://192.168.1.204:8801"
apolloConfigService = "http://192.168.1.204:8080"
namespace = "application"
apolloAccesskeySecret = ""
cluster = "seata"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
nodePath = "/seata/seata.properties"
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
- 在nacos中配置registry.conf中声明的 seata的配置文件seataServer.properties
# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/sovzn?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
- 创建数据库表 global_table、branch_table、lock_table
- 运行seata-server.bat 启动即可,在nacos 的服务列表会看到名为
seata-tc-server的服务

微服务集成Seata
引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本较低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
修改配置文件
需要修改每个微服务的application.yml文件,增加以下内容:
# 【注册中心配置】:微服务 TC 服务注册中心的配置,微服务根据这些信息去注册中心获取 TC 服务地址
seata:
registry:
# 参考tc服务自己的registry.conf中的配置
type: nacos
nacos: # tc
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
# Seata 服务器(TC)在 Nacos 中注册的服务名。微服务会根据这个名字去 Nacos 里查找 Seata 服务器。
application: seata-tc-server
# 对应的集群名称
cluster: syc
tx-service-group: seata-demo # 事务组名称 自定义
# 不同的微服务需要参与到同一个分布式事务中(比如订单服务调用库存服务一起完成下单),那么它们所有服务的 tx-service-group(事务组名称)都必须配置成一模一样的。
# 【配置中心配置】:微服务外部配置中心,拉取Seata框架的全局运行参数,规范当前微服务调用TC服务
config:
type: nacos # 指定配置中心的类型为 Nacos
nacos:
server-addr: 127.0.0.1:8848 # Nacos 配置中心服务器的 IP 地址和端口
username: nacos
password: nacos
group: DEFAULT_GROUP # 指定配置所在的分组
data-id: client.properties # 指定在 Nacos 中存放 Seata 客户端详细配置的配置文件名
在nacos中配置client.properties
# 【注】事务组映射关系 将微服务yaml中定义的事务组seata-demo映射到集群 syc,即:该事务组的事物由 syc 集群里的 TC服务来调度执行
service.vgroupMapping.seata-demo=syc
service.enableDegrade=false
service.disableGlobalTransaction=false
# 与TC服务的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
# RM配置
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
# TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
# undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100
Seata 四种模式
seata:
data-source-proxy-mode: AT
XA模式
(数据库原生协议模式)
核心原理:基于数据库原生的 XA 协议实现经典的两阶段提交(2PC),Seata 仅作为事务协调器(TC)。
执行流程:
- 一阶段Prepare(准备阶段):各数据库执行本地 SQL 但不提交,锁定资源,并向 TC 报告准备状态。
- 二阶段Commit/Rollback(提交/回滚阶段):若所有数据库准备成功,TC 通知全部提交并释放锁;若有失败,则通知全部回滚。
特点:保证强一致性,对业务无侵入,但由于一阶段全程持有数据库锁,性能最差,且依赖数据库对 XA 协议的支持。
适用场景:必须保证强一致性、服务少且事务短、全数据库支持 XA 的非高并发企业系统(如银行核心账务、证券交易、政府财务系统)。
如下代码, 扣用户余额或扣库存失败整体都会回滚
@Override
@GlobalTransactional
public Long create(Order order) {
// 创建订单
orderMapper.insert(order);
try {
// 扣用户余额
accountClient.deduct(order.getUserId(), order.getMoney());
// 扣库存
storageClient.deduct(order.getCommodityCode(), order.getCount());
} catch (FeignException e) {
log.error("下单失败,原因:{}", e.contentUTF8(), e);
throw new RuntimeException(e.contentUTF8(), e);
}
return order.getId();
}
AT 模式
(Automatic Transaction,自动事务模式)
- 核心原理:基于改进的两阶段提交模型,通过拦截业务 SQL,自动生成回滚日志(undo_log)和全局锁来实现自动补偿。
- 执行流程:
- 一阶段:执行业务 SQL直接提交 并生成前后镜像(undo_log表的
rollback_info字段),与业务数据在同一个本地事务中提交,随后释放本地锁和数据库连接。 - 二阶段:若全局提交,则异步快速清理删除 undo_log数据;若全局回滚,则根据 undo_log 生成反向补偿 SQL 恢复数据。
- 一阶段:执行业务 SQL直接提交 并生成前后镜像(undo_log表的
- 特点:对业务代码零侵入,实现简单,性能较好,但属于最终一致性。
- 适用场景:单体改微服务、业务逻辑简单(如 CRUD 操作)、一致性要求较高且开发资源有限的场景(如电商订单创建、企业内部管理系统)。
在 Seata AT 模式中,事务的隔离性保证与传统单机数据库有所不同。AT 模式的核心设计思想是在性能与一致性之间寻找平衡,因此它通过全局锁(Global Lock)来实现写隔离,而读隔离则默认较弱,需要依赖业务层面的配合。
以下是 AT 模式写隔离与脏读机制的详细解析:
1. 写隔离(防止脏写)
脏写:
脏写场景还原
假设数据库初始数据为 money = 100,事务1和事务2并发修改该数据:
事务1:获取本地数据库锁,保存更新前快照(money: 100),执行 SQL 将 money 更新为 90,随后提交本地事务并释放数据库锁(此时数据库值为 90)。
事务2:获取本地数据库锁,保存更新前快照(money: 90),执行 SQL 将 money 更新为 80,随后提交本地事务并释放数据库锁(此时数据库值为 80)。
触发回滚:此时如果事务1所在的全局事务失败,需要回滚。事务1会根据自己记录的快照将数据恢复为 100。
脏写发生:事务2的更新(80)被事务1的回滚操作(100)直接覆盖,导致事务2的修改丢失,这就是脏写。
为了解决脏写问题 AT 模式通过全局锁来防止脏写,其核心逻辑是“先放数据库锁,再卡全局锁”。
获取锁:当全局事务需要操作数据库资源时,会向
lock_table中插入一条锁记录。如果资源已被其他全局事务锁定,当前事务将进入等待重试状态,直到超时或资源被释放。释放锁:在全局事务二阶段完成提交或回滚后,Seata 会自动清除
lock_table中相应的锁记录。
lock_table通过xid(全局事务ID)和branch_id(分支事务ID)维护了与global_table和branch_table之间的关联。其核心字段包括
- 机制原理:在一阶段本地事务提交前,资源管理器(RM)必须先向事务协调器(TC)申请该数据行的全局锁。如果拿不到全局锁,就不能提交本地事务。
- 执行流程:
- 事务 T1 执行修改 SQL,获取本地数据库锁,执行完毕后在提交前获取全局锁,然后提交本地事务并释放本地锁(此时全局锁仍由 T1 持有)。
- 事务 T2 修改同一条数据,能正常获取本地锁并执行 SQL,但在提交前尝试获取全局锁时,发现锁被 T1 占用,T2 会进入等待重试状态。
- 如果 T1 全局提交,释放全局锁,T2 获取锁后继续提交;如果 T1 全局回滚,TC 会通知 T2 回滚,T2 根据 undo_log 撤销本地已提交的修改,从而避免脏写。
- 本质:用轻量级的“全局锁”替代了传统 XA 模式中长时间持有的“重量级数据库锁”,在保证事务正确性的同时,最大化了数据库的并发效率。
2. 读隔离(防止脏读)
AT 模式的读隔离机制分为默认状态和升级状态。
- 默认隔离级别:读未提交(Read Uncommitted)
在 AT 模式下,一阶段本地事务提交后,数据在数据库中就已经真实可见,但此时全局事务可能尚未完成二阶段决议。因此,普通的SELECT语句可能会读到其他全局事务未最终提交的数据(即脏读)。Seata 默认采用此级别,是为了避免全局锁竞争影响系统吞吐量。 - 升级为读已提交(Read Committed)
如果在特定业务场景下(如资金操作)不允许读到中间态数据,可以通过在查询方法上添加@GlobalLock注解并结合SELECT FOR UPDATE语句来实现:- 机制原理:
SELECT FOR UPDATE语句的执行会向 TC 申请全局锁。如果该数据的全局锁被其他事务持有,当前查询会释放本地锁并阻塞等待,直到全局锁释放(即前面的全局事务完成提交或回滚)后,才返回已提交的最新数据。 - 注意事项:
@GlobalLock必须结合SELECT FOR UPDATE使用才能生效。此外,这种悲观读锁方案会阻塞并发读,可能对性能产生影响,建议仅在强一致性要求的场景下使用。
- 机制原理:
与XA的区别
XA
- 锁载体:数据库原生行锁
- 持锁逻辑:一阶段 Prepare 加锁 → 全程持有 → 二阶段结束才释放
- 本质:悲观长锁,锁跟着分布式事务走,跨网络、跨阶段一直占着
- 效率:低,并发争抢热点数据时排队严重
AT
- 数据库锁:本地短行锁,SQL 执行完、本地事务提交就立刻释放,持锁只有毫秒级,和普通本地事务一致
- 全局排他标记:框架层面加的逻辑锁,只用来防多事务并发修改同一条数据(脏写),不阻塞数据库操作
- 本质:数据库短锁 + 轻量逻辑排他,没有长阻塞
- 效率:远高于 XA,适配高并发
TCC模式
(Try-Confirm-Cancel,预留确认取消模式)
核心原理:一种业务层面的两阶段提交,需要开发者手动实现 Try、Confirm、Cancel 三个操作。
执行流程
:
- Try(一阶段):检查业务合法性并预留资源(如冻结库存、冻结金额),不真正扣减。
- Confirm(二阶段):确认提交,真正执行业务操作(如扣减冻结库存)。
- Cancel(二阶段):取消回滚,释放预留的资源(如解冻库存)。
特点:无全局锁,性能极高,但代码侵入性强,需要手动编写补偿逻辑,且需考虑幂等和空回滚等问题。
适用场景:高并发核心业务、性能要求极高、跨数据库/中间件、需精细控制资源的场景(如电商秒杀、金融核心交易)。
一、空回滚(Empty Rollback)
- 产生原因:当某分支事务的 Try 阶段因网络拥堵阻塞、服务宕机或发生异常而未能成功执行时,全局事务可能因超时而触发回滚。此时,事务协调器(TC)会向该分支发送 Cancel 指令。由于 Try 阶段根本没有执行,Cancel 阶段实际上并没有预留的资源可以释放,这就形成了“空回滚”。
- 业务影响:如果 Cancel 方法没有识别出这是空回滚,直接执行了“释放资源”的逻辑(例如凭空增加了可用余额),就会导致数据不一致。
- 解决思路:Cancel 接口在执行前,必须能够识别出 Try 阶段是否已经执行过。如果 Try 未执行,Cancel 应直接返回成功,不做任何业务操作。
二、业务悬挂(Suspension)
- 产生原因:悬挂是空回滚的衍生问题。在发生空回滚后,如果之前因网络拥堵而阻塞的 Try 请求“迟到”了,并在 Cancel 执行之后才真正到达并执行成功(成功预留了资源)。此时,全局事务已经结束,后续再也不会有 Confirm 或 Cancel 来释放这部分被预留的资源,导致资源被永久“悬挂”。
- 业务影响:被悬挂的资源(如冻结的库存或资金)无法被再次使用,造成业务故障。
- 解决思路:Try 接口在执行前,必须检查该事务是否已经执行过 Cancel。如果 Cancel 已经执行过,Try 必须拒绝执行,从而避免资源悬挂。
示例 :余额扣除
冻结金额表
@Data
public class AccountFreeze {
@TableId(type = IdType.INPUT)
private String xid; // 全局事务ID(主键!)
private String userId; // 用户ID
private Integer freezeMoney; // 冻结金额
private State state;
public enum State {
TRY, CONFIRM, CANCEL
}
}
TCC 接口
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
@LocalTCC // 跨服务必加
public interface AccountTCCService {
@TwoPhaseBusinessAction(
name = "deductAccountTCC",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
void deduct(
BusinessActionContext context,
@BusinessActionContextParameter("userId") String userId,
@BusinessActionContextParameter("money") int money
);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
TCC 实现类
import io.seata.core.context.RootContext;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
@Resource
private AccountMapper accountMapper;
@Resource
private AccountFreezeMapper freezeMapper;
// ====================== Try 阶段 ======================
@Override
public void deduct(BusinessActionContext context, String userId, int money) {
// 0. 获取全局事务 XID
String xid = RootContext.getXID();
// ========== 【关键】防悬挂:如果 CANCEL 已经执行,直接拒绝 ==========
AccountFreeze freeze = freezeMapper.selectById(xid);
if (freeze != null && freeze.getState() == AccountFreeze.State.CANCEL) {
throw new RuntimeException("事务已回滚,拒绝执行Try");
}
// 1. 扣减可用余额,余额不足直接报错
accountMapper.deduct(userId, money);
// 2. 记录冻结记录,状态=TRY
AccountFreeze accountFreeze = new AccountFreeze();
accountFreeze.setXid(xid);
accountFreeze.setUserId(userId);
accountFreeze.setFreezeMoney(money);
accountFreeze.setState(AccountFreeze.State.TRY);
freezeMapper.insert(accountFreeze);
}
// ====================== Confirm 阶段 ======================
@Override
public boolean confirm(BusinessActionContext ctx) {
String xid = ctx.getXid();
// 幂等:已经删过了,直接返回成功
AccountFreeze freeze = freezeMapper.selectById(xid);
if (freeze == null) {
return true;
}
// 删除冻结记录 = 提交成功
int count = freezeMapper.deleteById(xid);
return count == 1;
}
// ====================== Cancel 阶段 ======================
@Override
public boolean cancel(BusinessActionContext ctx) {
String xid = ctx.getXid();
AccountFreeze freeze = freezeMapper.selectById(xid);
// ========== 【关键】空回滚:Try 没执行,直接返回成功 ==========
if (freeze == null) {
return true;
}
// 幂等:已经回滚过
if (freeze.getState() == AccountFreeze.State.CANCEL) {
return true;
}
// 1. 恢复余额
accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
// 2. 冻结记录清零 + 状态改为 CANCEL
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
int count = freezeMapper.updateById(freeze);
return count == 1;
}
}
调用方(订单服务)
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private AccountFeignClient accountFeignClient;
@GlobalTransactional // 开启全局事务
public void createOrder(String userId, int money) {
// 1. 创建订单
orderMapper.create(userId, money);
// 2. 跨服务扣余额
accountFeignClient.deduct(userId, money);
}
}
SAGA 模式
(长事务模式)
- 核心原理:将长事务拆分为一系列本地短事务,每个短事务都有对应的补偿操作。如果某个环节失败,按相反顺序执行补偿操作。
- 执行流程:
- 正向执行:按业务顺序依次执行各个本地短事务。
- 补偿执行:若某个短事务失败,从失败点开始反向执行各个补偿事务(需手动编写补偿)。
- 特点:适合长事务、无锁高并发、允许最终一致性,但补偿逻辑可能较为复杂。
- 适用场景:执行时间长(如大于1分钟)、涉及多个服务的复杂业务流程(如电商订单履约、银行跨行转账、企业工作流系统)。
- Post link: http://sovzn.github.io/2026/05/24/Seata/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.



若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues