news 2026/10/7 2:19:56

Seata四种分布式事务模式详解:从AT到TCC选型与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Seata四种分布式事务模式详解:从AT到TCC选型与实战

做后端的人迟早会碰上这么一个问题:明明下单接口在本地环境跑得好好的,一上微服务就成了“薛定谔的订单”——订单表里有一条记录,库存却还是满的。单体时代这种事根本不存在,一个数据库事务包起来,要么全成功要么全失败。但服务一拆,订单服务和库存服务各管各的库,本地事务只能约束自己那一段,跨服务的数据一致性一下就没了抓手。这也是为什么 Spring Cloud 体系里,Seata 几乎是分布式事务的默认答案。这篇文章不讲理论八股,直接以“订单创建 + 库存扣减”这个最经典的业务场景为例,把 Seata 的 AT、TCC、Saga、XA 四种模式挨个拆开讲,包括它们怎么落地、怎么选型,以及我在生产环境里踩过的坑。适合正在做 Spring Cloud 微服务、被数据一致性问题困扰的同学,花二十分钟读一遍,至少能少走一个月的弯路。

1. 微服务拆分之后,事务到底断在了哪里

1.1 从“一个事务”变成“一串操作”

单体应用里下单的逻辑很简单:开启事务,插入订单,扣减库存,更新余额,提交事务。所有操作都在同一个数据库连接上执行,数据库的 ACID 帮我们兜底,代码里甚至不用写太多补偿逻辑。微服务拆开之后,订单和库存变成了两个独立的服务、两个独立的数据库,原来那一个事务被拆成了两个乃至多个本地事务。下单接口先调订单服务写订单,再通过 OpenFeign 调库存服务扣库存,这两个操作之间没有任何机制能保证“要么都成功,要么都失败”。

很多团队在初期设计时根本意识不到这是问题,直到线上出现库存被超卖、订单状态和实际扣减对不上,才回过头来补课。从本质上看,微服务架构把“本地事务”换成了“跨服务调用链条”之后,事务的原子性边界被打破了。本地事务管不到对端,对端的异常对本地来说只是一个 RPC 响应码,本地事务只能决定自己提交还是回滚,没办法替对端做决定。这就是事务断层的根源。

1.2 分布式事务维护的到底是什么

要理解 Seata 在做什么,可以先想清楚一个问题:分布式事务维护的其实是“多个数据源之间的最终一致性”。单体事务追求的是强一致,提交那一刻所有数据要么全可见、要么全不可见。微服务场景下,大多数业务其实可以接受短时间内的数据不一致——订单先落库、库存暂未扣减,只要最终能对上账就行,这个过程就叫最终一致性(BASE 模型里的“软状态 + 最终一致”)。

分布式事务框架的核心工作,就是在这段“不一致窗口期”里做协调和兜底:如果全部服务都成功,那就把数据慢慢收敛到最终状态;如果有任一分支失败,就触发回滚或补偿,把已经写出去的数据改回来。Seata 的四种模式,本质上是在“一致性强度”和“性能损耗”之间做不同的取舍。AT 和 XA 偏向强一致,TCC 和 Saga 偏向业务可控的最终一致。后面会具体展开。

1.3 不是所有场景都该上分布式事务

这里必须先泼一盆冷水:分布式事务是有成本的,性能和开发复杂度都不低,不要一遇到多个服务写数据就无脑引入 Seata。我见过不少项目,明明可以通过接口设计规避,比如“先扣库存再生成订单”、“扣库存只发消息让订单服务异步落库”,硬是上了分布式事务,结果全局锁拖慢了接口,上线第一天就压测不过。

什么时候真的需要?我的判断标准很简单:跨服务、跨库写操作在同一业务请求里必须保持最终一致,且无法通过异步削峰或业务流程重排解决。典型的如订单创建扣库存、支付回调后更新订单与账务、资金转账。如果只是同步一个查询缓存、写一个日志表,完全没必要引入 Seata。先把方案设计做对,再谈框架选型,这是我第一个建议。

2. 先把 Seata 的整体骨架讲透:TC、TM、RM 与全局事务流转

2.1 Seata 的三个角色和部署形态

Seata 的架构里有三个角色,初次上手的人特别容易搞混。TC(Transaction Coordinator)是事务协调器,也就是我们要单独部署的 Seata Server,负责全局事务的创建、提交、回滚决策;TM(Transaction Manager)是事务管理器,嵌入在发起全局事务的那个服务里,一般就是指标了 @GlobalTransactional 的方法所在的服务;RM(Resource Manager)是资源管理器,负责管理各分支事务的资源,比如订单服务里操作数据库的那段逻辑就是一个 RM 分支。

部署形态上,Seata Server 是一个独立的 Java 进程,默认监听 8091 端口,可以注册到 Nacos、Consul、Eureka 等注册中心。我这边生产环境用的拓扑是:Nacos 做注册中心和配置中心,Seata Server 以集群方式部署,事务会话和全局锁信息存到 MySQL 里,客户端服务通过注册中心发现 Server 地址。这种部署方式的好处是客户端不用硬编码 Server 地址,Server 挂了还能靠集群内部的选举机制切换。

2.2 一次 @GlobalTransactional 的完整生命周期

拿“订单创建 + 库存扣减”来说,TM 在订单服务的 createOrder 方法上加了 @GlobalTransactional,整个流程可以拆成四个阶段。第一阶段,TM 向 TC 发起全局事务注册,TC 生成一个全局唯一的 XID,并把 XID 放入调用链上下文,通过 Feign 的请求头透传给库存服务。第二阶段,订单服务执行本地事务插入订单,库存服务收到 XID 后执行本地事务扣减库存,这两个本地事务各自提交,同时告诉 TC“我这个分支干完了”。

第三阶段,TC 收到所有分支的结果后做决策。如果都成功,TC 通知所有分支提交全局事务,这里的“提交”在不同模式下含义不同,AT 模式下就是把之前生成的 undo_log 清理掉。第四阶段,如果有分支失败,TC 通知所有分支回滚,各分支根据 undo_log 生成反向 SQL 恢复数据。整个流程就是典型的“两阶段提交”思想,但 Seata 在细节上做了大量优化,让大部分分支事务在一阶段就提交了本地事务,释放数据库行锁,只在需要回滚时才做补偿操作。

2.3 分支事务、全局锁与 undo_log 各自干什么

很多同学初次看 Seata 源码会觉得晕,其实只要抓住三个概念就行。分支事务是全局事务里每个参与者的本地事务,每个分支都有独立的 Branch ID,最终结果由 TC 汇总。全局锁是 Seata 在 AT 模式下引入的一种分布式锁,用来防止两个全局事务并发修改同一行数据时互相覆盖。undo_log 是每个参与 AT 模式的业务库必须要建的一张表,用来记录数据变更前后的快照。

把这三个概念串起来理解 AT 模式的回滚原理:一阶段业务修改数据前,Seata 通过 DataSourceProxy 拦截 SQL,把当前数据“前镜像”和“后镜像”写入 undo_log;如果二阶段需要回滚,就根据 undo_log 里的镜像数据生成反向 SQL,把数据恢复成修改前的样子。这也解释了为什么 AT 模式对业务代码几乎没有侵入——它是在数据源层面做的代理拦截,业务只需要加一个注解。

3. AT 模式实战:订单服务 + 库存服务落地全过程

3.1 环境准备:Seata Server 与注册配置中心

AT 模式是 Seata 最主打、也是我用得最多的模式,先把环境搭好。我用的是 Seata 1.8.x 主线版本,搭配 Spring Cloud Alibaba 2.2.x,注册中心和配置中心都走 Nacos。Seata Server 部署有两种常见方式,一种是下载官方发行包后执行 seata-server.sh 启动:

# 指定端口、注册方式、事务日志存储方式 ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m file

另一种是 Docker 方式,适合快速验证:

docker run --name seata-server \ -p 8091:8091 \ -e SEATA_IP=127.0.0.1 \ -e STORE_MODE=file \ apache/seata-server:1.8.0

我建议不要把事务日志存默认的 file 模式直接上生产,至少换成 DB 模式,把 global_table、branch_table、lock_table 三张表建到 MySQL 里,否则 Seata Server 一重启,正在执行中的全局事务信息就全丢了。这里有个细节:DB 模式下需要在 store 配置里指定数据库驱动、连接串、用户名密码,不同版本的建表 SQL 略有差异,最好到对应版本的源码目录里拿。

3.2 业务库准备:undo_log 表与数据源代理

AT 模式下,每个参与分支事务的业务数据库都必须建 undo_log 表,这是最容易漏的一步,漏了之后分支回滚会直接报错。MySQL 下的建表语句是官方标准模板:

CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

这里要补充两个实战心得。第一,undo_log 的 unique key 用的是 (xid, branch_id),保证每个分支事务只有一条回滚记录,重复执行时会冲突,这也是幂等性的一层保障。第二,字符集一定要用 utf8,不要图省事用 utf8mb4 之外自建的字集,否则特殊字符写入镜像数据时可能出问题。表建好之后,还要把数据源换成 Seata 代理数据源,低版本需要自己定义 DataSourceProxy Bean,高版本 seata-spring-boot-starter 在配置了 seata.data-source-proxy-mode: AT 之后会自动包装,我这边用的就是自动代理,省了不少样板代码。

3.3 核心实现:Feign 调用 + @GlobalTransactional 注解

环境就绪后,业务代码反而很简洁。订单服务里,库存扣减通过 OpenFeign 调用,核心方法标注 @GlobalTransactional:

@Service public class OrderService { @Resource private OrderMapper orderMapper; @Resource private InventoryClient inventoryClient; @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class) public void createOrder(OrderDTO orderDTO) { // 第一步:写订单主表和明细,本地事务 orderMapper.insert(orderDTO); // 第二步:远程扣减库存,库存服务同样参与同一个全局事务 inventoryClient.deduct(new DeductRequest(orderDTO.getProductId(), orderDTO.getCount())); } }

库存服务这边的扣减接口就是一个普通的业务方法,但从调用链上下文里能拿到 XID,因此它自己执行数据库操作时同样会被纳入全局事务管理。这里有个关键配置:Feign 调用必须开启 Seata 的上下文传递,Spring Cloud Alibaba 的集成包默认支持,不需要写额外拦截器,但要注意别在远程调用前把线程池、异步任务里的 XID 搞丢,异步线程里如果也需要参与事务,得手动做 XID 的绑定和恢复。

实际部署时还会遇到一个高频问题:@GlobalTransactional 的 name 属性对同一个服务里的不同方法要取不同名字,并且 rollbackFor 务必要写 Exception.class。Spring 的默认回滚策略只针对 RuntimeException 和 Error,如果 Feign 接口抛出的是 checked Exception,不设置 rollbackFor 全局事务不会回滚,这是新手踩得最多的坑之一。

3.4 AT 模式回滚的关键细节与注意事项

AT 模式看起来无侵入,但落地时有三条红线必须守住。第一,业务 SQL 必须能被 Seata 正确解析,简单的 INSERT、UPDATE、DELETE 都没问题,但如果 SQL 里带了函数、聚合、子查询、LIMIT,或者更新条件没有走唯一索引,Seata 生成前后镜像时可能解析失败或锁范围变大,回滚时也有风险。第二,不要在同一条数据上跨多个全局事务并发修改,AT 模式靠全局锁保证隔离性,两个事务同时抢同一行时,后到的一方会重试等待,重试超时报锁冲突,接口变慢。第三,分支事务执行完后,本地事务一定要提交,别自己手动 rollback,否则 undo_log 和数据状态对不上。

我生产上的一个真实案例:库存服务扣减逻辑用了 UPDATE stock SET count = count - ? WHERE product_id = ? AND count >= ?,这条 SQL 本身没问题,但因为 product_id 在线上库不是唯一索引,全局锁退化成范围锁,并发一高就频繁锁冲突。后来把库存表的主键改成 product_id,并把扣减条件简化,锁冲突明显下降。这类问题在压测之前很难暴露,所以建议所有参与 AT 模式的表,主键和更新条件都设计成唯一的,能少踩很多坑。

4. TCC / Saga / XA 三种模式怎么上手

4.1 TCC 模式:三方法手动管控,以库存冻结为例

TCC 模式把每个分支事务拆成 Try、Confirm、Cancel 三个方法,框架负责按顺序调用:Try 阶段做资源检查和预留,Confirm 阶段做正式提交,Cancel 阶段做补偿回滚。相比 AT 模式的自动生成镜像,TCC 把业务规则完全交给你自己,灵活度最高,但也意味着每个参与方都要写三套逻辑。库存扣减用 TCC 落地时,我的做法是在业务表之外再加一张冻结表,先把要扣的数量“冻住”:

@LocalTCC public interface InventoryTccAction { @TwoPhaseBusinessAction(name = "deductStockTcc", commitMethod = "confirmDeduct", rollbackMethod = "cancelDeduct") boolean tryDeduct( @BusinessActionContextParameter(paramName = "productId") Long productId, @BusinessActionContextParameter(paramName = "count") Integer count); boolean confirmDeduct(BusinessActionContext context); boolean cancelDeduct(BusinessActionContext context); }

实现类里,Try 阶段先校验库存余量,然后执行 UPDATE stock SET freeze_count = freeze_count + ? WHERE product_id = ? AND stock_count >= ?,成功就插入一条冻结记录;Confirm 阶段把冻结转成真实扣减,也就是把 stock_count、freeze_count 同步调整;Cancel 阶段把冻结数量释放回去。三个方法都要保证幂等,Confirm 和 Cancel 绝不能因为重复调用产生叠加效果。TCC 最大的好处是性能好,事务粒度可控,最大的坏处是开发成本高,而且空回滚(Try 还没成功就执行了 Cancel)、悬挂(Cancel 先于 Try 到达)这类问题都要自己防,一般用一张控制表记录 XID 和分支状态来解决。

4.2 Saga 模式:状态机编排的长事务补偿

Saga 模式是四种模式里最适合长事务的,它把一个全局事务拆成一串本地事务,每个本地事务都配一个补偿事务。执行到第 N 步失败时,从第 N-1 步开始反向执行补偿。Seata 的 Saga 主要基于状态机引擎实现,事务流程定义在一个 JSON 文件里,由引擎按定义逐步调用服务方法。比如先创建订单,再扣减库存,再扣减余额,每个节点都配一个 compensateState:

{ "name": "create-order-saga", "start": { "stateId": "createOrder" }, "states": { "createOrder": { "type": "ServiceTask", "serviceName": "orderService", "serviceMethod": "createOrder", "compensateState": "compensateOrder", "next": "deductStock" }, "deductStock": { "type": "ServiceTask", "serviceName": "inventoryService", "serviceMethod": "deductStock", "compensateState": "compensateDeduct", "next": "end" }, "compensateOrder": { "type": "ServiceTask", "serviceName": "orderService", "serviceMethod": "compensateOrder" }, "compensateDeduct": { "type": "ServiceTask", "serviceName": "inventoryService", "serviceMethod": "compensateDeduct" } } }

调用入口就一个方法,把状态机名字和业务参数丢进去即可。Saga 模式不需要全局锁,长事务也不会长时间占住数据库资源,这是它最大的优势。代价是什么?它牺牲了隔离性,别的请求在补偿完成前可能读到中间状态的数据,而且补偿逻辑的业务规则要自己设计,不像 AT 那样自动生成。我一般只在跨系统、跨公司接口的长流程里用 Saga,比如聚合支付平台的多渠道结算。

4.3 XA 模式:把一致性交给数据库

XA 模式是四种模式里最标准的强一致方案,它直接复用数据库原生支持的 XA 两阶段提交协议。MySQL 的 InnoDB、Oracle、PostgreSQL 都支持 XA,Seata 要做的就是把多个数据源纳入同一个全局事务,让数据库自己完成 prepare、commit、rollback 三个过程。XA 是真正的强一致,不存在中间脏数据,业务代码侵入比 TCC 小得多,只需要在配置里把数据源代理模式切到 XA:

seata: >
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 2:18:50

银河麒麟v10用CrossOver跑Windows exe:安装配置与避坑指南

简介:这份PDF资料面向在银河麒麟桌面版V10系统上需要运行Windows EXE应用的个人与企业用户,重点讲解借助CrossOver这一基于Wine的二进制翻译兼容层完成安装的完整思路。内容涵盖CrossOver工作原理、容器选择、安装包设置,并以WPS与QQ两个EXE为…

作者头像 李华
网站建设 2026/10/7 2:13:42

GitHub日榜上的知识型仓库:从howtolivebetter看开源知识管理趋势

今天照例刷了一遍GitHub日榜,九月末的榜单照旧很热闹。一眼扫过去,真正引发讨论的不是哪款新框架,而是几个知识型仓库:排在前面的是一个叫howtolivebetter的项目,副标题很直白——“高性价比人生指南”,直接…

作者头像 李华