资讯动态

SpringCloudAlibaba+Seata分布式事务实战:AT/TCC/Saga选型与避坑指南

发布时间:2026/10/26 4:39:55 来源:尧图企业网站定制
分布式事务这个话题我在过去几年里跟过好几个跨服务的数据一致性项目几乎每次都是从“看起来能跑”到“上线后对不上账”之间反复拉扯。SpringCloudAlibaba 这套组合里Seata 是最常被拎出来解决分布式事务的组件但真正落地的时候很多人会卡在几个地方AT 模式的全局锁到底锁了什么、TCC 的空回滚怎么防、Saga 的状态机怎么编排、以及最要命的——事务分组和注册中心配置对不上导致事务根本发不出去。这篇内容我打算把基于 SpringCloudAlibaba Seata 的分布式事务设计从头到尾拆一遍包括选型逻辑、核心机制、配置细节、踩坑排查链路以及几种模式在实际业务里的取舍。适合已经上手过 SpringCloud、正在或准备引入 Seata 的后端开发看也适合被分布式事务问题折腾过、想系统理一遍思路的同学。1. 为什么单靠本地事务在微服务里必然翻车1.1 一个订单场景把问题摊开先别急着上 Seata得先搞清楚问题到底出在哪。假设一个下单链路拆成了三个服务订单服务创建订单、库存服务扣减库存、账户服务扣减余额。每个服务各自连自己的库各自用Transactional管本地事务。单看每个服务事务都是完整的但把三个拼在一起问题就来了。订单服务本地事务提交成功之后调用库存服务。如果库存服务扣减成功、账户服务扣减失败订单已经落库了库存也扣了钱没扣成——数据就不一致了。反过来订单服务提交后调用库存服务超时订单服务不知道库存到底扣没扣重试可能重复扣不重试可能漏扣。这就是分布式系统里经典的“部分失败”问题没有一个横跨多个服务的本地事务能把这些操作包成一个原子单元。很多人第一反应是“加个 try-catch失败了就回滚”。但本地事务的回滚只能回滚自己库里的数据回滚不了已经通过 RPC 调用出去、在别的服务里提交了的事务。这就是为什么单靠本地事务在微服务架构里必然翻车——事务的边界和服务的边界不重合。1.2 CAP 与 BASE 决定了你不可能既要又要再往深一层想分布式事务本质上是在 CAP 之间做取舍。网络分区P在分布式环境里是客观存在的你没法消除。那么在分区发生时你要么保一致性C牺牲可用性A要么保可用性牺牲强一致性。绝大多数互联网业务选的是后者也就是 BASE 理论基本可用、软状态、最终一致。这个取舍直接决定了技术选型。如果你做的是银行核心账务这种强一致场景那可能得用刚性事务方案比如基于 XA 的两阶段提交代价是性能和可用性。如果你做的是电商下单、积分发放这类场景最终一致就够了那 Seata 的 AT、TCC、Saga 这些柔性事务方案就是更合适的选择。先想清楚业务能容忍多大程度的不一致、能容忍多久的不一致再谈用什么组件这个顺序不能反。1.3 Seata 在整套方案里扮演的角色Seata 的核心思路是引入一个全局事务协调者TCTransaction Coordinator由它来协调各个分支事务的提交和回滚。业务侧通过 TMTransaction Manager开启全局事务RMResource Manager负责管理分支事务资源并向 TC 注册。三个角色分工明确TM 管全局边界RM 管本地资源TC 管协调决策。这个模型的好处是把“跨服务的一致性”从业务代码里抽出来交给框架协调。但要注意Seata 不是银弹它解决的是“协调”问题不解决“业务幂等”和“补偿逻辑正确性”问题。后面讲 TCC 和 Saga 的时候你会看到业务侧的补偿逻辑写错了框架再强也救不回来。2. AT 模式最省事但也最容易被误解的方案2.1 两阶段提交的“改良版”到底改了什么AT 模式是 Seata 里用得最多的模式因为它对业务侵入最小——你几乎不用改业务代码加个GlobalTransactional注解就能跑。但“省事”的背后是一套挺精巧的机制不理解它就容易在出问题时抓瞎。AT 模式本质是两阶段提交的改良。传统 XA 的两阶段提交第一阶段所有参与者都锁住资源等协调者指令第二阶段才提交或回滚锁持有时间长性能差。AT 模式的做法是第一阶段就把本地事务提交了同时把更新前后的数据快照记到 undo_log 表里第二阶段如果是全局提交异步删掉 undo_log 就行如果是全局回滚就根据 undo_log 里的快照生成反向 SQL 把数据改回去。关键点在于第一阶段本地事务已经提交了所以本地锁释放了但 Seata 会在 TC 侧维护一把全局锁。这把全局锁保证在全局事务结束前其他事务不能修改同一行数据。这就是 AT 模式既能保证一致性、又比 XA 性能好的原因——本地锁释放得早全局锁只在 TC 侧做逻辑校验。2.2 undo_log 表的结构与回滚原理undo_log 是 AT 模式的命根子表结构大致是这样CREATE TABLE undo_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info LONGBLOB NOT NULL, log_status INT NOT NULL, log_created DATETIME(6) NOT NULL, log_modified DATETIME(6) NOT NULL, UNIQUE KEY ux_undo_log (xid, branch_id) );rollback_info里存的是序列化后的前后镜像beforeImage 和 afterImage。回滚的时候Seata 拿 beforeImage 和当前数据做比对如果当前数据和 afterImage 一致说明这期间没被别人改过可以安全回滚如果不一致说明有脏写会抛异常。这个比对机制是 AT 模式防脏写的核心。这里有个实操细节很多人不知道undo_log 表必须在每个参与分支的业务库里都建不是只在某个中心库建。我见过有团队只在订单库建了结果库存服务回滚时找不到 undo_log 表直接报错。另外不同数据库的建表语句有差异MySQL、Oracle、PostgreSQL 各有一套别拿 MySQL 的脚本往 PG 上怼。2.3 全局锁的粒度与隔离级别陷阱AT 模式的默认隔离级别是读未提交Read Uncommitted因为第一阶段本地事务已提交其他事务能读到还没全局提交的数据。Seata 提供了GlobalLock注解和SELECT ... FOR UPDATE的代理来加强隔离但很多人不知道这个默认值以为 AT 模式天然是读已提交结果在并发场景下读到了中间态数据。全局锁的粒度是行级锁的 key 由表名 主键值组成。这意味着如果你的业务表没有主键或者用了联合主键但 Seata 配置没对上全局锁可能失效或者锁错行。我踩过一次坑有张表用了业务字段做逻辑主键但物理上没建主键AT 模式回滚时定位不到行直接报错。AT 模式对表结构的硬性要求是必须有主键这个在接入前就得确认。2.4 AT 模式适用与不适用的边界AT 模式适合什么场景简单 CRUD、没有复杂业务校验、对性能要求不是极致、能接受读未提交隔离级别的场景。比如下单扣库存、发优惠券这种。不适合什么场景第一涉及非关系型存储的操作AT 模式依赖 SQL 解析和 undo_logRedis、MongoDB 这些它管不了。第二业务逻辑里有大量非数据库副作用发短信、调第三方接口AT 回滚只能回滚数据库回滚不了已经发出去的短信。第三高并发写同一行数据的场景全局锁会成为瓶颈。这几种情况就得上 TCC 或 Saga。3. TCC 模式把补偿逻辑的控制权交回业务3.1 Try-Confirm-Cancel 三段式的本质TCC 把每个分支事务拆成三个阶段Try 做资源检查和预留Confirm 做真正的业务提交Cancel 做资源释放。和 AT 模式最大的区别是TCC 的资源预留和提交是业务自己实现的框架只负责协调调用。举个例子扣库存的 TCC 实现Try 阶段把库存从“可用”挪到“冻结”Confirm 阶段把冻结的库存真正扣掉Cancel 阶段把冻结的库存还回可用。这样即使后面某个分支失败Cancel 也能把预留的资源释放掉不会出现超卖。TCC 的好处是控制力强、性能好没有全局锁、没有 undo_log 开销坏处是侵入性大——每个参与方都得写三个方法而且这三个方法都得保证幂等、防悬挂、防空回滚。这三防是 TCC 落地的真正难点。3.2 空回滚、悬挂、幂等三座大山空回滚Try 还没执行比如网络超时导致 Try 没到达Cancel 却先到了。如果 Cancel 不做判断直接执行可能把本来就没预留的资源“释放”一遍造成数据错误。解决办法是在 Try 之前先插一条事务控制记录Cancel 时检查这条记录存不存在不存在就说明 Try 没执行直接返回成功。悬挂Try 超时了TC 判定全局回滚Cancel 先执行了结果 Try 后来又到了把资源预留了但全局事务已经结束这个预留的资源就永远挂在那了。解决办法是 Cancel 执行时也插一条记录Try 执行前检查有没有 Cancel 记录有就拒绝执行。幂等Confirm 和 Cancel 都可能被重试调用必须保证重复调用结果一致。通常用事务控制表的唯一键来兜底重复插入直接捕获异常返回成功。下面是一个事务控制表加 Try 防悬挂的示意// Try 阶段先检查是否已被 Cancel public boolean tryDeduct(String bizId, int amount) { // 唯一键 biz_id action 防重复 int inserted tccControlMapper.insertIgnore(bizId, TRY); if (inserted 0) { // 已存在 TRY 或 CANCEL 记录 if (tccControlMapper.existsCancel(bizId)) { throw new IllegalStateException(已被取消拒绝悬挂); } return true; // 幂等返回 } // 执行资源预留 return stockMapper.freeze(bizId, amount) 0; }这段代码看着简单但每一行都是踩坑换来的。insertIgnore用唯一键兜幂等existsCancel防悬挂缺一个都可能在压测或网络抖动时出问题。3.3 TCC 的选型判断什么时候值得付出这个复杂度TCC 不是越用越好。我的判断标准是当业务需要精确的资源预留、或者操作对象不是关系型数据库、或者对性能有较高要求时才值得上 TCC。如果只是简单的数据库 CRUDAT 模式能覆盖就别上 TCC三防逻辑写起来和维护成本都不低。还有一个容易被忽略的点TCC 的 Confirm 和 Cancel 必须是幂等的而且最好能异步化。Confirm 阶段如果同步调用下游很慢会拖长全局事务时间。有些团队会把 Confirm 做成异步消息驱动但这又引入了消息可靠性问题得配合本地消息表或者事务消息来保证。这块的复杂度要提前评估。4. Saga 模式长流程编排的另一种思路4.1 状态机驱动的正向与补偿链路Saga 模式的核心是把一个长事务拆成一串本地事务每个本地事务都有对应的补偿操作正向执行失败就反向执行已成功步骤的补偿。和 TCC 的区别在于Saga 没有“资源预留”的概念它是“先做了再说错了再补”。Seata 的 Saga 实现是基于状态机的用 JSON 定义状态图每个状态节点是一个服务调用节点之间定义正向和补偿的转换关系。这种声明式编排的好处是流程可视化、易调整适合跨多个服务的长流程比如旅游预订机票 酒店 租车、跨行转账这类。状态机定义大致长这样{ Name: orderSaga, StartState: CreateOrder, States: { CreateOrder: { Type: ServiceTask, ServiceName: orderService, ServiceMethod: create, CompensateState: CancelOrder, Next: DeductStock }, DeductStock: { Type: ServiceTask, ServiceName: stockService, ServiceMethod: deduct, CompensateState: RestoreStock, Next: Succeed } } }正向链路 CreateOrder → DeductStock如果 DeductStock 失败状态机会自动调 CancelOrder 补偿。这个编排逻辑是持久化的即使服务重启也能从断点继续这是 Saga 相比手写补偿链路的一大优势。4.2 Saga 与 TCC 的取舍控制力 vs 编排能力Saga 和 TCC 经常被拿来比较。简单说TCC 控制力强资源预留精确但侵入大、要写三防Saga 编排能力强长流程、可视化、可持久化恢复但补偿是“事后补救”中间态对业务可见隔离性弱。选型上如果是短链路、资源竞争激烈的场景比如秒杀扣库存TCC 更合适如果是长链路、跨多个异构系统、流程经常调整的场景比如订单履约、审批流Saga 的状态机编排更省心。我做过一个跨五个服务的履约流程用 Saga 状态机把正向和补偿都画出来之后产品和运营都能看懂流程改起来也方便这是 TCC 给不了的。4.3 补偿逻辑的幂等与“补偿失败”兜底Saga 最大的风险是补偿失败。补偿操作本身也可能因为网络、下游故障而失败如果补偿一直失败数据就卡在中间态了。所以 Saga 落地必须配一套补偿失败的兜底机制要么重试到成功配合告警要么进人工处理队列。补偿逻辑同样要幂等。因为状态机会重试补偿同一个补偿可能被执行多次。我的做法是每个补偿操作都带一个业务流水号补偿前先查这个流水号有没有处理过处理过就直接返回。另外补偿操作最好设计成“可重入”的比如“恢复库存”用stock stock n就不如用“把冻结的 n 还回可用”这种基于状态的写法安全前者重试会多加。5. 环境搭建与配置那些文档里不会强调的细节5.1 Seata Server 的部署与存储模式选择Seata ServerTC的部署有三种存储模式file、db、redis。file 模式最简单配置写在本地文件里适合开发和单机测试但重启会丢全局事务状态生产绝对不能用。db 模式把事务状态存到数据库支持集群是生产的主流选择。redis 模式性能好但要注意持久化配置不然宕机丢数据。db 模式需要初始化三张表global_table、branch_table、lock_table。这三张表是 TC 协调全局事务的核心建表脚本在 Seata 发行包的script/server/db目录下。我见过有人图省事用 file 模式上生产结果 TC 重启后一批未完成的全局事务全部丢失分支事务卡在半提交状态最后只能人工对账修复。生产环境老老实实用 db 模式这是底线。5.2 事务分组与注册中心的对齐这是配置里最容易出错的地方。Seata 的事务分组transaction service group是客户端和 TC 之间的逻辑映射客户端配置spring.cloud.alibaba.seata.tx-service-groupTC 侧配置service.vgroupMapping.groupdefault。这两边的名字必须严格对上差一个字符事务就发不出去。更隐蔽的坑是注册中心。客户端通过注册中心找到 TC 的地址如果 TC 注册到 Nacos 的 namespace/group 和客户端配置的不一致客户端就发现不了 TC。表现是全局事务开启后分支事务注册不上日志里报can not get cluster name或者no available service。排查这类问题第一步就是确认 TC 在注册中心里的服务名、namespace、group 和客户端配置完全一致。# 客户端配置示例 seata: enabled: true application-id: order-service tx-service-group: order_tx_group service: vgroup-mapping: order_tx_group: default registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: seata-ns group: SEATA_GROUP5.3 数据源代理AT 模式能生效的前提AT 模式依赖数据源代理来拦截 SQL、生成 undo_log。SpringCloudAlibaba 集成 Seata 时数据源必须被 Seata 的DataSourceProxy包一层否则 SQL 拦截不到undo_log 不会生成回滚也就无从谈起。用 starter 的话seata-spring-boot-starter会自动代理数据源但前提是你的数据源是通过标准方式配置的。如果你自己手动 new 了一个 DataSource 或者用了多数据源框架自动代理可能失效。判断方法很简单启动后看日志里有没有Auto proxy of DataSource字样没有就说明没代理上。多数据源场景下每个数据源都要单独代理这块配置要格外小心。6. 踩坑排查实录从现象到根因的完整链路6.1 全局事务不回滚先查 XID 有没有传下去现象订单服务抛异常了但库存服务的扣减没回滚。排查链路是这样的第一步看订单服务的日志确认GlobalTransactional生效了、全局事务开启了、XID 生成了。第二步看调用库存服务时 XID 有没有通过 RPC 上下文传过去。Seata 靠拦截器把 XID 塞进 RPC 的 attachment 里如果用了自定义的 RPC 框架或者手动拼的 HTTP 调用拦截器可能没生效XID 就断了。XID 断了的表现是库存服务的分支事务注册到了一个“孤儿”全局事务上或者根本没注册。日志里能看到库存服务的 branchId 和订单服务的 XID 对不上。解决办法是确认 RPC 框架的 Seata 拦截器已启用手动调用的话要自己把 XID 从上下文取出来塞进请求头。6.2 回滚报“脏写”全局锁没拦住并发现象全局回滚时抛BranchRollbackFailed提示 afterImage 和当前数据不一致。根因是回滚前有别的非全局事务改了同一行数据全局锁没拦住。为什么会没拦住因为全局锁只对“也走 Seata 全局事务”的写操作生效如果有个定时任务或者别的系统直接连库改数据Seata 是拦不住的。这类问题的处理思路第一确认所有会写这张表的入口都走了 Seata 全局事务第二对确实无法接入的旁路写操作要么改成走全局事务要么在业务上避免和全局事务并发写同一行。实在避免不了的得接受 AT 模式在这个场景下的局限考虑换 TCC 用业务锁来控制。6.3 TC 连不上注册中心与网络的双重确认现象客户端启动报can not connect to services-server。排查顺序先确认 TC 进程活着、端口通再确认 TC 注册到注册中心的服务名和客户端配置的一致最后确认客户端能访问注册中心。这三步里最常出问题的是第二步——TC 注册的 cluster name 和客户端 vgroupMapping 映射的 cluster 对不上。还有一种情况是 TC 集群部署了多节点但客户端只发现了一个节点负载不均衡导致部分请求超时。这时候要检查注册中心里 TC 的实例列表是否完整以及 TC 的store.db配置是否所有节点一致db 模式下多节点共享同一个库配置必须一致。7. 生产落地时的几个关键决策7.1 模式选型的决策表把前面几种模式的取舍整理成一张表选型时对着看会清晰很多维度AT 模式TCC 模式Saga 模式业务侵入低高三方法三防中补偿逻辑隔离性读未提交需额外控制资源预留隔离性好弱中间态可见性能中全局锁高中适用场景简单 CRUD资源竞争激烈、非DB资源长流程、多系统编排补偿可靠性框架自动业务保证业务保证状态机重试落地复杂度低高中选型的核心判断顺序是先看操作对象是不是关系型数据库不是就排除 AT再看链路长短和是否需要可视化编排长链路优先 Saga最后看资源竞争和隔离要求高并发写同一行优先 TCC。7.2 幂等、重试、对账三件套不能省不管用哪种模式生产环境都得配三样东西幂等所有分支操作和补偿操作都要幂等、重试网络抖动导致的失败要能自动重试、对账定时核对关键数据的一致性兜住框架兜不住的漏。对账这块特别想说一句不要迷信框架能保证 100% 一致。再好的分布式事务方案在极端情况下比如 TC 宕机、消息丢失、人工误操作都可能出现不一致。对账是最后一道防线发现不一致后走人工或自动修复流程。我参与过的项目里对账任务救回来的数据不一致案例比框架自动回滚解决的还多。7.3 监控与告警全局事务的可观测性Seata 的全局事务状态、分支事务成功率、回滚次数这些指标要接入监控。TC 侧有 metrics 暴露客户端侧可以埋点统计GlobalTransactional方法的耗时和结果。重点监控两个指标全局事务回滚率突然升高说明下游有系统性问题和分支事务超时率升高说明某个服务变慢可能拖垮全局事务。告警阈值建议按业务基线来定比如回滚率超过 5% 就告警。另外 TC 的lock_table如果出现大量长时间未释放的全局锁说明有全局事务卡住了这个也要监控不然会拖垮整个系统的写性能。8. 一些踩过之后才明白的经验AT 模式最省事但它的“省事”是有前提的——表必须有主键、所有写入口都得走全局事务、能接受读未提交。这三个前提缺一个省事就变成埋雷。我现在的习惯是接入 AT 之前先把这几条在团队里对齐避免上线后才发现某个旁路任务在裸写数据库。TCC 的三防逻辑别想着“应该不会那么巧”。网络抖动、服务重启、TC 重试这些在压测和真实流量里都会遇到。事务控制表是必须的而且唯一键要设计对biz_id action这种组合能同时兜住幂等和悬挂检查。Saga 的状态机编排最大的价值不是技术上的而是让流程变得“可讨论”。把正向和补偿画出来之后业务方也能参与评审补偿逻辑对不对这在 TCC 那种纯代码实现里是做不到的。如果你的流程经常变、涉及多方Saga 的编排能力值得多花点时间。最后说配置。Seata 的配置问题占了线上故障的一大半而且报错信息往往很模糊。我的排查顺序固定为XID 有没有传下去 → 事务分组和注册中心对不对 → 数据源有没有被代理 → TC 连不连得上。按这个顺序走大部分问题能在十分钟内定位。这套顺序不是文档里写的是被坑出来的希望对正在接入 Seata 的你有点用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑