SpringCloud 进阶拓展:分布式事务终极解决方案 Seata AT/TCC 模式全栈实战(含生产级避坑指南)

本文作者:多年微服务架构实战经验,深耕分布式事务落地,从双 11 核心链路踩坑沉淀而来,全文无废话、全干货,从原理到源码级拆解,从环境搭建到生产落地,从单模式使用到混合模式实战,再到 90% 开发者都会踩的坑,一篇带你吃透 Seata 分布式事务,看完即可直接落地到项目中。本文适合人群Java 初中级开发工程师、微服务架构师、需要落地分布式事务的技术负责人、准备面试的求职者

原创不易,点赞、收藏、关注三连,后续持续更新 Seata 高可用、性能优化、混沌工程验证系列内容

一、开篇:微服务架构下,分布式事务为什么是绕不开的坎?

1.1 从单体到微服务,事务的本质发生了什么变化?

在单体架构中,我们所有的业务都在一个服务里,操作的是同一个数据库,一个@Transactional注解就能搞定事务的 ACID 特性,保证 "要么全成功,要么全回滚"。

但当我们做了微服务拆分,情况完全变了:

  • 一个下单流程,会拆分出订单服务、库存服务、账户服务、支付服务,每个服务都是独立部署的节点
  • 每个服务都对应自己的数据库,甚至是不同类型的数据库(MySQL、Redis、MongoDB)
  • 服务之间通过 HTTP/RPC 远程调用,网络抖动、服务宕机随时可能发生

这时候,单体的本地事务完全失效了 ------ 订单创建成功了,库存扣减了,结果账户余额扣减失败了,数据就出现了不一致,这就是分布式事务要解决的核心问题。

1.2 分布式事务的核心痛点:一致性 vs 性能

分布式事务的本质,是在不可靠的分布式环境下,保证多个独立资源操作的原子性,而它天生就面临 CAP 定理的约束:

  • 一致性 (Consistency):所有节点在同一时间看到的数据是一致的
  • 可用性 (Availability):服务一直可用,正常请求能在合理时间内返回结果
  • 分区容错性 (Partition Tolerance):网络分区故障发生时,系统依然能正常运行

在微服务架构中,P 是必须满足的,我们只能在 C 和 A 之间做权衡,而分布式事务的所有方案,本质上都是在 C 和 A 之间找一个适合业务的平衡点。

而传统的分布式事务方案,都有无法回避的硬伤:

方案 核心原理 硬伤
原生 2PC/3PC 数据库原生两阶段提交,强一致 性能极差,长事务占用数据库连接,容易出现阻塞和死锁,不适合高并发场景
本地消息表 本地事务 + 消息队列实现最终一致 代码侵入性强,表结构与业务强耦合,运维成本高,不支持复杂事务场景
SAGA 模式 长事务拆分,正向执行 + 反向补偿 无隔离性,脏写问题难以解决,补偿逻辑开发量巨大,出问题难以排查
最大努力通知 基于消息队列的定时重试通知 一致性最弱,只适合非核心场景(如短信通知、站内信)

1.3 为什么说 Seata 是分布式事务的终极解决方案?

Seata 是阿里开源的分布式事务框架,历经双 11 万亿级流量的考验,目前已经成为 Apache 顶级项目,也是国内微服务架构下分布式事务的事实标准。

它的核心优势,完美解决了传统方案的痛点:

  1. 多模式适配全场景:支持 AT、TCC、SAGA、XA 四大模式,覆盖 99% 的业务场景,简单场景用 AT 零侵入,高并发复杂场景用 TCC 高性能
  2. 极低的业务侵入性 :AT 模式下,业务代码几乎零改造,一个@GlobalTransactional注解就能开启全局事务,开发成本极低
  3. 高性能低延迟:AT 模式一阶段直接提交本地事务,释放锁资源,二阶段异步化处理,性能远超原生 2PC,完全适配高并发场景
  4. 完善的生态兼容:完美适配 SpringCloud、SpringCloud Alibaba、Dubbo 等主流微服务框架,支持 MySQL、Oracle、PostgreSQL 等主流数据库
  5. 生产级高可用:支持 TC 集群部署、注册中心 / 配置中心对接、全链路监控告警,经过互联网大厂多年生产环境验证

接下来,我们就从核心原理入手,一步步带你落地 Seata 的 AT/TCC 模式,吃透分布式事务。

二、Seata 核心基石:架构与核心概念全解

2.1 Seata 是什么?

Seata (Simple Extensible Autonomous Transaction Architecture) 是一款高性能的开源分布式事务中间件,致力于在微服务架构下提供简单易用、高性能的分布式事务服务。

从开源至今,Seata 已经迭代到 2.x 稳定版本,在阿里内部支撑了双 11 的核心交易链路,在金融、电商、物流等多个行业有大规模的生产落地,是目前国内最成熟的分布式事务解决方案。

2.2 Seata 三大核心组件:分布式事务的 "三剑客"

Seata 的分布式事务,本质上是由三大核心组件协同完成的,这三个组件的职责分工,是理解 Seata 所有模式的基础。

这里先给大家配上 Seata 核心架构图,一眼看懂组件间的协同关系:

三个组件的核心职责,我用最直白的话给大家讲清楚:

  1. TC (Transaction Coordinator) - 事务协调器

    • 全局事务的 "大脑",独立部署的 Seata Server 服务
    • 负责维护全局事务的状态,接收 TM 的请求,创建全局事务并生成唯一的 XID(全局事务 ID)
    • 协调所有分支事务的执行,根据分支事务的执行结果,决定全局事务是提交还是回滚
    • 管理全局锁的分配与释放,保证事务的隔离性
  2. TM (Transaction Manager) - 事务管理器

    • 全局事务的 "发起者",一般在业务的入口服务(比如订单服务的下单接口)
    • 通过@GlobalTransactional注解标记全局事务的边界,负责向 TC 申请开启、提交、回滚全局事务
    • 负责将 XID 通过服务调用链路传递给下游的 RM,保证整个分布式事务链路的 XID 统一
  3. RM (Resource Manager) - 资源管理器

    • 分布式事务的 "执行者",部署在每个微服务节点中,负责管理分支事务
    • 负责分支事务的本地执行,向 TC 注册分支事务,上报分支事务的执行状态
    • 接收 TC 的提交 / 回滚指令,执行对应的本地事务提交或回滚操作
    • AT 模式下,负责生成 undo_log 回滚日志,TCC 模式下,负责调用自定义的 Confirm/Cancel 逻辑

2.3 Seata 四大模式核心对比:AT/TCC/SAGA/XA 怎么选?

Seata 提供了四种事务模式,分别适配不同的业务场景,这里给大家做一个全维度的对比,看完就知道什么场景该用什么模式:

模式 核心原理 侵入性 性能 一致性 适用场景
AT 模式 基于本地关系型数据库,通过数据源代理自动生成 undo_log,两阶段自动化提交 / 回滚 极低,几乎零侵入 极高 最终一致 90% 的常规业务场景,基于 MySQL 等支持本地 ACID 的关系型数据库,希望业务代码零改造
TCC 模式 手动实现 Try-Confirm-Cancel 三个阶段,资源预留 + 确认提交 / 取消释放,完全由业务代码控制 高,需手动实现三个阶段逻辑 极高 最终一致 高并发核心链路(如秒杀、库存扣减)、跨异构数据库(MySQL+Redis)、非关系型数据库操作、AT 模式无法覆盖的复杂业务
SAGA 模式 长事务拆分为多个正向操作,每个正向操作对应一个反向补偿操作,失败时按顺序反向补偿 高,需手动实现正向 + 反向逻辑 最终一致 超长事务流程(如金融审批、物流全链路)、无法改造的老系统接入、事务参与者多且流程长的场景
XA 模式 基于数据库原生 XA 协议实现的强一致两阶段提交,一阶段 prepare,二阶段 commit/rollback 低,零侵入 强一致 金融、银行等对数据强一致要求极高的场景,并发量不高,对性能要求不苛刻

我们重点讲解AT 模式和 TCC 模式,这两个模式覆盖了 99% 的生产业务场景,也是大家面试和工作中最常用的。

三、Seata AT 模式:零侵入的分布式事务方案,从原理到源码级拆解

AT 模式是 Seata 最核心、最常用的模式,也是大家入门 Seata 的首选模式,它最大的优势就是业务代码零侵入,完全自动化实现分布式事务的提交和回滚,开发成本极低。

3.1 AT 模式的核心设计理念

AT 模式的本质,是对传统 2PC 的革命性优化,它解决了传统 2PC 的性能痛点:

  • 传统 2PC:一阶段执行 SQL 不提交,持有数据库锁,等待所有分支完成,二阶段才提交 / 回滚,锁持有时间长,性能极差
  • Seata AT 模式:一阶段就提交本地事务,释放本地锁,二阶段如果是提交,异步删除 undo_log 即可;如果是回滚,通过 undo_log 自动生成反向 SQL 补偿,锁持有时间极短,性能大幅提升

而支撑这个设计的,就是 AT 模式的两大核心机制:基于数据源代理的 undo_log 自动生成机制 + 全局锁机制

3.2 AT 模式两阶段提交全流程详解

这里我用最经典的电商下单场景(订单创建 + 库存扣减),给大家拆解 AT 模式的完整执行流程,同时配上 AT 模式两阶段执行流程图,一眼看懂整个过程。

3.2.1 一阶段:业务执行 + 快照数据保存

一阶段是 AT 模式的核心,也是它性能远超传统 2PC 的关键,整个过程分为 6 个核心步骤:

  1. 开启全局事务 :TM(订单服务)执行被@GlobalTransactional标记的方法,向 TC 发起请求,开启全局事务,TC 生成唯一的全局事务 XID,并返回给 TM
  2. XID 链路传递:TM 通过 RPC/HTTP 调用下游的库存服务,将 XID 传递到下游服务,保证整个链路的 XID 统一
  3. 分支事务注册:RM(库存服务)接收到请求,发现请求头中包含 XID,说明当前处于全局事务中,向 TC 注册分支事务,生成分支事务 ID(Branch ID),并关联到当前的 XID
  4. SQL 拦截与镜像生成 :RM 通过数据源代理(DataSourceProxy)拦截业务 SQL(比如库存扣减的 UPDATE 语句),解析 SQL 类型、表名、条件、更新字段,先查询更新前的数据,生成前镜像(Before Image) ;然后执行业务 SQL,再查询更新后的数据,生成后镜像(After Image)
  5. 生成 undo_log 并本地提交 :根据前后镜像,生成 undo_log 回滚日志,将业务数据的更新和 undo_log 的写入,放在同一个本地事务中提交,提交完成后,立即释放本地数据库锁
  6. 上报执行结果:RM 向 TC 上报当前分支事务的执行结果(成功 / 失败)

这里要划重点:AT 模式一阶段就提交了本地事务,释放了本地锁,这是它性能高的核心原因

3.2.2 二阶段:提交 / 回滚,自动化处理

TC 会收集所有分支事务的执行结果,根据结果决定全局事务是提交还是回滚,两个场景的处理逻辑完全不同:

场景 1:全部分支执行成功,全局提交

  • TC 向所有 RM 下发分支提交的指令
  • RM 接收到提交指令后,不需要做任何数据操作,只需要异步删除对应的 undo_log 日志即可,整个过程非常快,完全不影响业务性能

场景 2:有分支执行失败,全局回滚

  • TC 向所有 RM 下发分支回滚的指令
  • RM 接收到回滚指令后,根据 XID 和 Branch ID 查询对应的 undo_log 日志
  • 脏写校验:对比当前数据库中的数据和 undo_log 中的后镜像,判断数据是否被其他事务修改,如果出现脏写,会触发告警和人工处理
  • 校验通过后,根据前镜像生成反向 SQL,执行反向 SQL 完成数据回滚
  • 回滚完成后,删除对应的 undo_log 日志,向 TC 上报回滚结果

3.3 AT 模式的核心保障:全局锁 + 本地锁 + undo_log 机制

很多人会问:AT 模式一阶段就提交了本地事务,释放了本地锁,那如果有其他事务修改了同一条数据,回滚的时候不就出问题了吗?

这就是 Seata AT 模式的核心设计:本地锁保证本地事务的 ACID,全局锁保证分布式事务的隔离性

全局锁机制详解

全局锁是由 TC 管理的,记录的是 "哪个全局事务,锁住了哪个表的哪条数据",它的核心规则是:

  • 分支事务在一阶段提交本地事务之前,必须先拿到这条数据的全局锁,拿不到全局锁,就不能提交本地事务,必须回滚本地事务并释放本地锁
  • 全局锁只有在全局事务完成(提交 / 回滚)之后,才会被释放
  • 非 Seata 管理的事务,因为不会申请全局锁,所以能修改数据,这就是我们后面要讲的脏写坑

我用一个经典的并发场景,给大家讲清楚全局锁是如何避免脏写和超卖的:

  1. 全局事务 TX1,要扣减商品 A 的库存,一阶段执行 UPDATE 语句,拿到本地锁,执行业务 SQL,然后向 TC 申请商品 A 这条数据的全局锁,拿到全局锁后,提交本地事务,释放本地锁,全局锁不释放
  2. 此时,全局事务 TX2 也要扣减商品 A 的库存,执行 UPDATE 语句,拿到本地锁,执行业务 SQL,然后向 TC 申请全局锁,发现全局锁被 TX1 持有,申请失败,不断重试
  3. 如果 TX1 执行成功,TC 发起全局提交,提交完成后释放全局锁,TX2 就能拿到全局锁,完成自己的事务
  4. 如果 TX1 有分支执行失败,TC 发起全局回滚,TX1 拿到本地锁,根据 undo_log 回滚数据,回滚完成后释放全局锁和本地锁,TX2 继续执行

这样就保证了:在全局事务完成之前,其他分布式事务无法修改同一条数据,避免了并发场景下的脏写和数据不一致

undo_log 表核心设计

undo_log 是 AT 模式回滚的基础,每个业务库都必须创建 undo_log 表,它的核心字段如下:

sql 复制代码
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context, such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT          NOT NULL COMMENT '0:normal status, 1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`),
    KEY `ix_log_created` (`log_created`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

核心字段说明:

  • xid + branch_id:唯一索引,定位到具体的分支事务回滚日志
  • rollback_info:存储了前后镜像的二进制数据,是回滚的核心依据
  • log_status:日志状态,0 是正常状态,1 是防御状态(用于脏写校验)

3.4 AT 模式的适用场景与局限性

适用场景
  • 基于支持本地 ACID 特性的关系型数据库(MySQL、Oracle、PostgreSQL 等)
  • 常规的 CRUD 业务场景,希望业务代码零改造,开发成本低
  • 并发量较高,对性能有要求,能接受最终一致性的业务场景
  • 微服务架构下,多服务跨库操作的常规业务场景(如电商下单、支付、退款等)
局限性
  • 只支持关系型数据库,不支持 Redis、MongoDB 等非关系型数据库
  • 不支持跨异构数据源的复杂事务场景
  • 高并发热点数据场景下,全局锁竞争会导致性能下降(比如秒杀场景的商品库存)
  • 不能通过非 Seata 代理的数据源修改业务表,否则会绕过全局锁检查,导致脏写,回滚失败

四、Seata TCC 模式:高性能高灵活的分布式事务方案,原理与核心问题解决

如果说 AT 模式是 Seata 的 "普惠方案",那 TCC 模式就是 Seata 的 "高阶方案",它完全由业务代码控制,灵活性极高,性能极强,能解决 AT 模式无法覆盖的所有复杂场景,也是大厂高并发核心链路的首选方案。

4.1 TCC 模式的核心设计理念

TCC(Try-Confirm-Cancel)模式,本质上是一种侵入式的、完全由业务代码实现的两阶段提交方案,它把一个分布式事务拆分为三个阶段,每个阶段的逻辑都由开发者手动实现:

  • Try 阶段:尝试执行,完成所有业务检查,预留必须的业务资源(比如冻结库存、冻结账户余额)
  • Confirm 阶段 :确认执行,真正执行业务操作,只使用 Try 阶段预留的资源,必须保证幂等性
  • Cancel 阶段 :取消执行,释放 Try 阶段预留的资源,必须保证幂等性,同时处理空回滚和悬挂问题

TCC 模式的核心优势,就是完全摆脱了数据库的限制,不管是关系型数据库、非关系型数据库、还是第三方接口调用,只要能实现这三个阶段,就能纳入分布式事务的管理,同时它没有全局锁的竞争,性能远超 AT 模式,完美适配高并发热点场景。

4.2 TCC 模式的三个阶段:Try-Confirm-Cancel 全流程

同样用电商库存扣减的场景,给大家拆解 TCC 模式的三个阶段的核心职责,同时配上 TCC 模式执行流程图:

三个阶段的核心职责,我给大家讲透,这是 TCC 模式落地的关键:

  1. Try 阶段:检查 + 预留

    • 这是 TCC 的一阶段,核心是所有业务的前置检查资源预留
    • 必须完成所有的业务合法性校验:比如账户是否存在、余额是否充足、库存是否足够
    • 必须完成资源的预留,而不是直接扣减:比如冻结账户余额、冻结库存,而不是直接扣减可用余额 / 库存
    • 举个例子:库存扣减的 Try 阶段,不是直接把可用库存从 100 减到 90,而是把可用库存减 10,冻结库存加 10,总库存不变,这就是资源预留
  2. Confirm 阶段:确认提交

    • 这是 TCC 的二阶段提交,只有所有分支的 Try 阶段都执行成功,TC 才会下发 Confirm 指令
    • 核心是真正执行业务提交,只能使用 Try 阶段预留的资源,不能做任何业务检查
    • 必须保证幂等性:因为 TC 会重复下发 Confirm 指令,所以 Confirm 接口必须支持重复调用不重复执行
    • 接上面的例子:Confirm 阶段,把冻结的 10 个库存,真正扣减掉,冻结库存减 10,总库存减 10
  3. Cancel 阶段:取消回滚

    • 这是 TCC 的二阶段回滚,只要有一个分支的 Try 阶段执行失败,TC 就会给所有已经执行成功 Try 的分支下发 Cancel 指令
    • 核心是释放 Try 阶段预留的资源,回滚到初始状态
    • 必须保证幂等性 ,同时必须处理空回滚悬挂问题
    • 接上面的例子:Cancel 阶段,把冻结的 10 个库存恢复到可用库存,冻结库存减 10,可用库存加 10,回到初始状态

4.3 TCC 模式三大经典问题:空回滚、幂等、悬挂,根本原因与终极解决方案

TCC 模式虽然灵活高性能,但它有三个绕不开的经典问题,90% 的开发者在落地 TCC 的时候都会踩坑,这也是面试的高频考点,今天我给大家讲透根本原因和终极解决方案。

先给大家配上 TCC 三大问题场景图,一眼看懂问题发生的过程:

问题 1:幂等性问题

根本原因:TC 在下发 Confirm/Cancel 指令后,会等待 RM 的返回结果,如果因为网络抖动、服务宕机,导致 TC 没有收到返回结果,TC 会重复下发指令,导致 Confirm/Cancel 方法被多次调用,如果不做幂等处理,就会出现重复扣减、重复回滚的问题。

解决方案

  • 基于 TCC 事务控制表,记录每个分支事务的执行状态,Confirm/Cancel 执行前,先查询状态,如果已经是 committed/rollbacked 状态,直接返回成功,不执行业务逻辑
  • Seata 1.5.1 + 版本已经内置了 TCC Fence 能力,只需要在@TwoPhaseBusinessAction注解中设置useTCCFence = true,就能自动实现幂等性,无需手动开发
问题 2:空回滚问题

根本原因:分支事务的 Try 阶段因为网络问题,请求没有到达 RM,或者 RM 在执行 Try 之前就宕机了,导致 Try 方法没有执行,但是 TC 因为超时,认为 Try 失败,下发了 Cancel 指令,RM 需要执行 Cancel 方法,但是此时没有预留任何资源,Cancel 无法回滚,这就是空回滚。

解决方案

  • Cancel 执行时,先检查 TCC 事务控制表中是否有对应的 trying 状态记录,如果没有,说明 Try 方法没有执行,属于空回滚,直接返回成功,不执行业务回滚逻辑,同时插入一条 suspended 状态的记录,防止后续的悬挂问题
  • 同样,TCC Fence 已经内置了空回滚的处理逻辑,无需手动开发
问题 3:悬挂问题

根本原因:这是空回滚的后续问题,Cancel 指令比 Try 指令先到达 RM,RM 执行了空回滚,返回成功,TC 认为全局事务已经结束,此时延迟的 Try 请求才到达 RM,执行了资源预留,但是全局事务已经结束了,不会再有 Confirm/Cancel 指令下发,导致预留的资源永远被冻结,这就是悬挂。

解决方案

  • Cancel 执行空回滚时,插入一条 suspended 状态的记录,标记这个 xid+branch_id 的事务已经执行过 Cancel 了
  • Try 方法执行时,先检查 TCC 事务控制表中是否有对应的记录,如果有 suspended 状态的记录,说明已经执行过 Cancel 了,直接拒绝执行 Try 方法,防止悬挂
  • TCC Fence 同样内置了防悬挂的处理逻辑,一键开启即可

4.4 TCC 模式的适用场景与局限性

适用场景
  • 高并发热点数据场景:比如秒杀、库存扣减,没有全局锁竞争,性能远超 AT 模式
  • 跨异构数据源的事务场景:比如同时操作 MySQL 和 Redis,AT 模式不支持,TCC 可以完美适配
  • 非关系型数据库的事务场景:比如 MongoDB、Elasticsearch 等,AT 模式不支持
  • 调用第三方接口的事务场景:比如调用支付、退款接口,需要保证接口调用的一致性
  • AT 模式无法覆盖的复杂业务场景
局限性
  • 代码侵入性极强,每个分支事务都需要手动实现 Try/Confirm/Cancel 三个方法,开发成本是 AT 模式的 3 倍以上
  • 对开发者的要求极高,需要充分理解 TCC 的三个阶段,处理好幂等、空回滚、悬挂三大问题
  • 业务逻辑和事务逻辑强耦合,后期维护成本高

五、环境前置准备:Seata Server + SpringCloud 项目整合全流程(含版本兼容避坑)

讲完了原理,接下来我们进入实战环节,第一步就是环境搭建,这也是 90% 的开发者踩坑的重灾区,我会给大家讲清楚每一个步骤,以及对应的避坑点。

5.1 版本选型终极指南(生产级必看)

Seata 最常见的坑,就是版本不兼容,客户端和服务端版本不一致、SpringCloud Alibaba 和 Seata 版本不匹配,都会导致事务不生效、回滚失败、甚至服务启动失败。

这里我给大家整理了 2026 年最新的生产级稳定版本对应关系,大家直接照着选,绝对不会踩坑:

SpringCloud Alibaba 版本 SpringCloud 版本 SpringBoot 版本 Seata Server 版本 JDK 版本 生产推荐
2025.1.0.0 2025.1.1 4.0.2+ 2.2.0 21+ 最新稳定推荐
2023.0.3.0 2023.0.2 3.2.x 2.1.0 17+ 主流稳定推荐
2022.0.0.0-RC2 2022.0.0 3.0.x 1.7.1 17+ 老项目兼容
2.2.10.RELEASE Hoxton.SR12 2.3.x 1.6.1 8+ 老项目维护

版本避坑铁则(生产环境必须遵守)

  1. Seata 客户端和服务端的大版本必须一致,比如服务端用 2.1.0,客户端必须用 2.1.x,严禁跨大版本混用,否则会出现协议不兼容、事务状态异常
  2. SpringCloud Alibaba 2.2.0.RELEASE 是分界点,之前的版本依赖seata-all,之后的版本依赖seata-spring-boot-starter,严禁混用
  3. 生产环境严禁使用 SNAPSHOT 版本,必须使用 RELEASE 稳定版
  4. JDK 版本必须和 SpringBoot 版本匹配,SpringBoot 3.0 + 必须用 JDK17+,SpringBoot 4.0 + 必须用 JDK21+

本文我们使用生产主流稳定版本进行实战:

  • SpringCloud Alibaba:2023.0.3.0
  • SpringCloud:2023.0.2
  • SpringBoot:3.2.5
  • Seata Server:2.1.0
  • Nacos:2.3.2(注册中心 + 配置中心)
  • MySQL:8.0
  • JDK:17

5.2 Seata Server(TC) 集群部署实战(基于 Nacos 注册 + 配置中心 + MySQL 存储)

Seata Server 就是我们说的 TC 事务协调器,生产环境必须部署集群,保证高可用,我们使用 Nacos 作为注册中心和配置中心,MySQL 作为存储介质,实现 TC 的集群部署。

5.2.1 数据库初始化

首先,我们需要在 MySQL 中创建 Seata 的数据库,初始化对应的表结构,Seata 的存储模式有 file、db、redis 三种,生产环境推荐使用 db 模式。

  1. 创建数据库
sql 复制代码
CREATE DATABASE seata DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE seata;
  1. 初始化表结构,SQL 脚本可以从 Seata 官网获取,核心表如下:
sql 复制代码
-- 全局事务表,记录所有全局事务的状态
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_status_gmt_modified` (`status`, `gmt_modified`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

-- 分支事务表,记录所有分支事务
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

-- 全局锁表,管理AT模式的全局锁
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT 0 COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid_and_branch_id` (`xid`, `branch_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

-- TCC Fence表,用于TCC模式的三大问题处理
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
    `xid`           VARCHAR(128)  NOT NULL COMMENT 'global id',
    `branch_id`     BIGINT        NOT NULL COMMENT 'branch id',
    `action_name`   VARCHAR(64)   NOT NULL COMMENT 'action name',
    `status`        TINYINT       NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
    `gmt_create`    DATETIME(3)   NOT NULL COMMENT 'create time',
    `gmt_modified`  DATETIME(3)   NOT NULL COMMENT 'update time',
    PRIMARY KEY (`xid`, `branch_id`),
    KEY `idx_gmt_modified` (`gmt_modified`),
    KEY `idx_status` (`status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
5.2.2 Nacos 配置同步

我们将 Seata Server 的配置同步到 Nacos 配置中心,方便集群节点统一管理,步骤如下:

  1. 从 Seata 官网下载 2.1.0 版本的安装包,解压后找到/script/config-center/config.txt文件,这是 Seata 的核心配置文件
  2. 修改核心配置:
bash 复制代码
# 存储模式,使用db
store.mode=db
# 数据库类型
store.db.dbType=mysql
# 驱动类
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 数据库连接地址
store.db.url=jdbc:mysql://你的MySQL地址:3306/seata?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
# 数据库用户名
store.db.user=你的数据库用户名
# 数据库密码
store.db.password=你的数据库密码
# 最小连接数
store.db.minConn=5
# 最大连接数
store.db.maxConn=30
# 全局事务查询次数
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.lockTable=lock_table
store.db.queryLimit=100
store.db.maxWait=5000

# 服务配置,事务分组,必须和客户端的tx-service-group对应
service.vgroupMapping.my_test_tx_group=default
# 注册中心配置
registry.type=nacos
registry.nacos.application=seata-server
registry.nacos.serverAddr=你的Nacos地址:8848
registry.nacos.group=SEATA_GROUP
registry.nacos.namespace=public
registry.nacos.username=nacos用户名
registry.nacos.password=nacos密码

# 配置中心配置
config.type=nacos
config.nacos.serverAddr=你的Nacos地址:8848
config.nacos.group=SEATA_GROUP
config.nacos.namespace=public
config.nacos.username=nacos用户名
config.nacos.password=nacos密码
  1. 执行/script/config-center/nacos/nacos-config.sh脚本,将配置同步到 Nacos 配置中心
5.2.3 Seata Server 配置与启动
  1. 解压 Seata Server 安装包,修改/conf/application.yml文件,核心配置如下:
bash 复制代码
server:
  port: 8091
spring:
  application:
    name: seata-server
logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
console:
  user:
    username: seata
    password: seata
seata:
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 你的Nacos地址:8848
      group: SEATA_GROUP
      namespace: public
      username: nacos用户名
      password: nacos密码
  config:
    type: nacos
    nacos:
      server-addr: 你的Nacos地址:8848
      group: SEATA_GROUP
      namespace: public
      username: nacos用户名
      password: nacos密码
  store:
    mode: db
  1. 启动 Seata Server:
    • Linux/Mac:执行/bin/seata-server.sh
    • Windows:执行/bin/seata-server.bat
  2. 启动成功后,访问http://你的服务器地址:8091,使用 seata/seata 登录控制台,能看到服务信息,说明启动成功
5.2.4 集群高可用配置

生产环境需要部署至少 3 个 TC 节点,实现集群高可用,步骤非常简单:

  1. 在另外两台服务器上,部署相同的 Seata Server,修改端口为 8092、8093
  2. 配置文件完全一样,都指向同一个 Nacos 和同一个 MySQL 数据库
  3. 启动三个节点,三个节点会自动注册到 Nacos,形成集群
  4. 客户端只需要配置 Nacos 地址,会自动发现 TC 集群节点,无需手动配置多个 TC 地址

5.3 SpringCloud 微服务项目整合 Seata 全步骤

接下来,我们将 SpringCloud 微服务项目整合 Seata 客户端,这里我会给大家讲清楚每一个步骤,以及对应的避坑点。

5.3.1 核心依赖引入(避坑:依赖冲突解决)

首先,在父工程的 pom.xml 中统一管理版本,然后在每个微服务子工程中引入核心依赖:

父工程 pom.xml 版本管理

XML 复制代码
<properties>
    <java.version>17</java.version>
    <spring-boot.version>3.2.5</spring-boot.version>
    <spring-cloud.version>2023.0.2</spring-cloud.version>
    <spring-cloud-alibaba.version>2023.0.3.0</spring-cloud-alibaba.version>
    <seata.version>2.1.0</seata.version>
    <mysql.version>8.0.33</mysql.version>
    <mybatis-plus.version>3.5.7</mybatis-plus.version>
</properties>

<dependencyManagement>
    <dependencies>
        <!-- SpringBoot 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- SpringCloud 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- SpringCloud Alibaba 依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- Seata 依赖 -->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>${seata.version}</version>
        </dependency>
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <!-- MyBatis-Plus 依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

子工程核心依赖引入

XML 复制代码
<dependencies>
    <!-- SpringBoot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Nacos 服务注册发现 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- OpenFeign 远程调用 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- Seata 客户端核心依赖 -->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
    </dependency>
    <!-- 排除SpringCloud Alibaba内置的Seata,避免版本冲突 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <exclusions>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>
    <!-- MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

依赖避坑重点

  • 必须排除spring-cloud-starter-alibaba-seata内置的seata-spring-boot-starter,否则会出现版本冲突,导致服务启动失败
  • 严禁同时引入seata-allseata-spring-boot-starter,否则会出现类冲突
  • 所有微服务的 Seata 客户端版本必须和 Seata Server 版本一致,否则会出现协议不兼容
5.3.2 客户端配置详解

在每个微服务的application.yml中,添加 Seata 的核心配置:

bash 复制代码
spring:
  application:
    name: order-service # 服务名称,每个服务不一样
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://你的MySQL地址:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: 数据库用户名
    password: 数据库密码
  cloud:
    nacos:
      discovery:
        server-addr: 你的Nacos地址:8848
        namespace: public
        username: nacos用户名
        password: nacos密码

# Seata核心配置
seata:
  # 事务分组,必须和Seata Server中配置的vgroupMapping对应
  tx-service-group: my_test_tx_group
  service:
    # 事务分组映射,key是上面的tx-service-group,value是集群名称
    vgroup-mapping:
      my_test_tx_group: default
  registry:
    # 注册中心类型,和Seata Server保持一致
    type: nacos
    nacos:
      server-addr: 你的Nacos地址:8848
      group: SEATA_GROUP
      namespace: public
      username: nacos用户名
      password: nacos密码
  config:
    # 配置中心类型,和Seata Server保持一致
    type: nacos
    nacos:
      server-addr: 你的Nacos地址:8848
      group: SEATA_GROUP
      namespace: public
      username: nacos用户名
      password: nacos密码
  # AT模式配置
  client:
    rm:
      # 全局锁重试次数
      lock-retry-times: 30
      # 全局锁重试间隔
      lock-retry-interval: 100
      # 异步提交,提升性能
      async-commit-buffer-limit: 1000
    tm:
      # 全局事务默认超时时间,单位毫秒
      default-global-transaction-timeout: 60000

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 日志配置
logging:
  level:
    io.seata: debug

配置避坑重点

  • tx-service-group必须和 Seata Server 中配置的service.vgroupMapping.xxx=default中的 xxx 完全一致,否则会出现can not get cluster name in registry config报错,服务无法注册到 TC
  • 注册中心和配置中心的配置,必须和 Seata Server 完全一致,包括 group、namespace、用户名密码,否则无法发现 TC 服务
  • 生产环境必须调整全局锁的重试次数和间隔,避免高并发下锁等待超时
5.3.3 数据源代理配置(AT 模式核心,90% 的人在这里踩坑)

AT 模式的核心是数据源代理,Seata 需要代理业务的数据源,才能拦截 SQL,生成 undo_log,实现自动回滚。

SpringBoot 3.0 + 版本,Seata 已经自动实现了数据源代理,无需手动配置,但是有几个必须遵守的规则,否则会导致数据源代理失效,AT 模式不回滚:

  1. 严禁手动配置DataSourceProxy,否则会出现双重代理,导致代理失效
  2. 严禁使用其他的数据源代理框架,比如动态数据源,必须将动态数据源交给 Seata 代理
  3. 所有操作业务表的 SQL,必须通过 Seata 代理的数据源执行,严禁通过原生数据源、JDBC 直连等方式操作业务表
  4. 每个业务库,必须创建 undo_log 表,否则 AT 模式会报错

多数据源 / 动态数据源避坑配置:如果你的项目使用了多数据源 / 动态数据源,必须手动配置 Seata 的数据源代理,核心配置如下:

java 复制代码
@Configuration
public class SeataDataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.dynamic.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.dynamic.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    public DataSource dynamicDataSource() {
        DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", masterDataSource());
        dataSourceMap.put("slave", slaveDataSource());
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        // 交给Seata代理数据源
        return new DataSourceProxy(dynamicDataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return factoryBean.getObject();
    }
}

六、AT 模式实战:经典电商场景(订单 - 库存 - 账户)全流程落地

环境搭建完成后,我们进入 AT 模式的实战环节,用最经典的电商下单场景,带大家完整落地 AT 模式,实现订单创建、库存扣减、账户余额扣减的分布式事务,保证要么全成功,要么全回滚。

6.1 业务场景与表结构设计

6.1.1 业务流程说明

我们的业务流程是:用户下单,创建订单 → 扣减商品库存 → 扣减用户账户余额,三个操作分属三个不同的微服务,三个不同的数据库,需要通过 Seata AT 模式保证分布式事务的一致性。

服务模块划分:

  • order-service:订单服务,负责创建订单,是全局事务的发起者(TM)
  • storage-service:库存服务,负责扣减商品库存,是分支事务的执行者(RM)
  • account-service:账户服务,负责扣减用户账户余额,是分支事务的执行者(RM)
6.1.2 数据库表设计
  1. 订单库 order_db:订单表 + undo_log 表
sql 复制代码
-- 订单表
CREATE TABLE `t_order` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `order_no` varchar(64) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `product_id` bigint NOT NULL COMMENT '商品ID',
  `count` int NOT NULL COMMENT '购买数量',
  `amount` decimal(10,2) NOT NULL COMMENT '订单金额',
  `status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0-创建,1-已支付,2-已取消',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- undo_log表,必须创建
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context, such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT          NOT NULL COMMENT '0:normal status, 1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`),
    KEY `ix_log_created` (`log_created`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
  1. 库存库 storage_db:库存表 + undo_log 表
sql 复制代码
-- 库存表
CREATE TABLE `t_storage` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '库存ID',
  `product_id` bigint NOT NULL COMMENT '商品ID',
  `total_count` int NOT NULL DEFAULT '0' COMMENT '总库存',
  `available_count` int NOT NULL DEFAULT '0' COMMENT '可用库存',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';

-- 初始化测试数据
INSERT INTO `t_storage` (`product_id`, `total_count`, `available_count`) VALUES (1, 100, 100);

-- undo_log表,必须创建
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context, such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT          NOT NULL COMMENT '0:normal status, 1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`),
    KEY `ix_log_created` (`log_created`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
  1. 账户库 account_db:账户表 + undo_log 表
sql 复制代码
-- 账户表
CREATE TABLE `t_account` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '账户ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '账户余额',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户表';

-- 初始化测试数据
INSERT INTO `t_account` (`user_id`, `balance`) VALUES (1, 1000.00);

-- undo_log表,必须创建
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context, such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT          NOT NULL COMMENT '0:normal status, 1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`),
    KEY `ix_log_created` (`log_created`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

6.2 微服务模块划分

我们创建三个微服务模块,分别是 order-service、storage-service、account-service,每个模块的结构如下:

bash 复制代码
├── order-service        # 订单服务(TM)
│   ├── src/main/java
│   │   ├── controller   # 控制器
│   │   ├── service      # 业务服务
│   │   ├── mapper       # 数据访问层
│   │   ├── entity       # 实体类
│   │   ├── feign        # 远程调用接口
│   │   └── OrderServiceApplication.java # 启动类
│   └── src/main/resources
│       └── application.yml # 配置文件
├── storage-service      # 库存服务(RM)
│   └── 结构同上
└── account-service      # 账户服务(RM)
    └── 结构同上

6.3 核心代码实现

6.3.1 账户服务(Account Service)
  1. 实体类 Account
java 复制代码
@Data
@TableName("t_account")
public class Account implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private BigDecimal balance;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
  1. Mapper 接口 AccountMapper
java 复制代码
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
    /**
     * 扣减账户余额
     */
    @Update("UPDATE t_account SET balance = balance - #{amount} WHERE user_id = #{userId} AND balance >= #{amount}")
    int deductBalance(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
}
  1. 服务接口与实现
java 复制代码
public interface AccountService extends IService<Account> {
    /**
     * 扣减账户余额
     */
    boolean deduct(Long userId, BigDecimal amount);
}

@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {

    @Override
    public boolean deduct(Long userId, BigDecimal amount) {
        int rows = baseMapper.deductBalance(userId, amount);
        return rows > 0;
    }
}
  1. 控制器 AccountController
java 复制代码
@RestController
@RequestMapping("/account")
public class AccountController {

    @Resource
    private AccountService accountService;

    @PostMapping("/deduct")
    public ResponseEntity<Boolean> deduct(@RequestParam Long userId, @RequestParam BigDecimal amount) {
        boolean result = accountService.deduct(userId, amount);
        return ResponseEntity.ok(result);
    }
}
  1. Feign 接口 AccountFeignClient,供订单服务调用
java 复制代码
@FeignClient(value = "account-service", fallbackFactory = AccountFeignFallbackFactory.class)
public interface AccountFeignClient {

    @PostMapping("/account/deduct")
    Boolean deduct(@RequestParam("userId") Long userId, @RequestParam("amount") BigDecimal amount);
}

// 降级工厂:必须抛出异常,不能静默返回,否则Seata无法感知分支失败
@Component
public class AccountFeignFallbackFactory implements FallbackFactory<AccountFeignClient> {
    @Override
    public AccountFeignClient create(Throwable cause) {
        return (userId, amount) -> {
            throw new RuntimeException("账户服务调用异常:" + cause.getMessage(), cause);
        };
    }
}

避坑前置提醒:Feign 降级逻辑必须抛出 RuntimeException,严禁返回 null/false 等默认值,否则 Seata 会判定分支事务执行成功,不会触发全局回滚,这是 AT 模式 90% 回滚失效的核心原因。

6.3.2 库存服务(Storage Service)
  1. 实体类 Storage
java 复制代码
@Data
@TableName("t_storage")
public class Storage implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.AUTO)
    private Long id;
    private Long productId;
    private Integer totalCount;
    private Integer availableCount;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
  1. Mapper 接口 StorageMapper
java 复制代码
@Mapper
public interface StorageMapper extends BaseMapper<Storage> {
    /**
     * 扣减可用库存
     */
    @Update("UPDATE t_storage SET available_count = available_count - #{count} WHERE product_id = #{productId} AND available_count >= #{count}")
    int deductStorage(@Param("productId") Long productId, @Param("count") Integer count);
}
  1. 服务接口与实现
java 复制代码
public interface StorageService extends IService<Storage> {
    boolean deduct(Long productId, Integer count);
}

@Service
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements StorageService {

    @Override
    public boolean deduct(Long productId, Integer count) {
        int rows = baseMapper.deductStorage(productId, count);
        return rows > 0;
    }
}
  1. 控制器 StorageController
java 复制代码
@RestController
@RequestMapping("/storage")
public class StorageController {

    @Resource
    private StorageService storageService;

    @PostMapping("/deduct")
    public ResponseEntity<Boolean> deduct(@RequestParam Long productId, @RequestParam Integer count) {
        boolean result = storageService.deduct(productId, count);
        return ResponseEntity.ok(result);
    }
}
  1. Feign 接口 StorageFeignClient,供订单服务调用
java 复制代码
@FeignClient(value = "storage-service", fallbackFactory = StorageFeignFallbackFactory.class)
public interface StorageFeignClient {

    @PostMapping("/storage/deduct")
    Boolean deduct(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

@Component
public class StorageFeignFallbackFactory implements FallbackFactory<StorageFeignClient> {
    @Override
    public StorageFeignClient create(Throwable cause) {
        return (productId, count) -> {
            throw new RuntimeException("库存服务调用异常:" + cause.getMessage(), cause);
        };
    }
}

6.3.3 订单服务(Order Service,全局事务发起者 TM)
  1. 实体类 Order
java 复制代码
@Data
@TableName("t_order")
public class Order implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.AUTO)
    private Long id;
    private String orderNo;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal amount;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
  1. Mapper 接口 OrderMapper
java 复制代码
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
  1. 核心业务实现(AT 模式核心入口)
java 复制代码
public interface OrderService extends IService<Order> {
    String createOrder(Long userId, Long productId, Integer count, BigDecimal unitPrice);
}

@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    @Resource
    private OrderMapper orderMapper;
    @Resource
    private StorageFeignClient storageFeignClient;
    @Resource
    private AccountFeignClient accountFeignClient;

    /**
     * AT模式分布式事务核心入口
     * @GlobalTransactional 开启全局分布式事务
     * name:全局事务唯一名称,建议按业务场景命名
     * rollbackFor = Exception.class:所有异常均触发回滚(必加!)
     */
    @Override
    @GlobalTransactional(name = "order-create-tx", rollbackFor = Exception.class)
    public String createOrder(Long userId, Long productId, Integer count, BigDecimal unitPrice) {
        // 1. 获取全局事务XID,生产环境必须打印,用于问题排查
        String xid = RootContext.getXID();
        log.info("【全局事务开启】XID:{}", xid);

        // 2. 生成订单编号
        String orderNo = "ORDER_" + System.currentTimeMillis() + "_" + userId;
        // 3. 计算订单总金额
        BigDecimal totalAmount = unitPrice.multiply(new BigDecimal(count));

        // 4. 本地事务:创建订单
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setProductId(productId);
        order.setCount(count);
        order.setAmount(totalAmount);
        order.setStatus(0);
        orderMapper.insert(order);
        log.info("【订单创建成功】订单号:{}", orderNo);

        // 5. 远程调用:扣减商品库存(分支事务1)
        Boolean storageResult = storageFeignClient.deduct(productId, count);
        if (!storageResult) {
            throw new RuntimeException("库存扣减失败:商品可用库存不足");
        }
        log.info("【库存扣减成功】商品ID:{}, 扣减数量:{}", productId, count);

        // 【回滚测试点】放开注释,手动触发异常,验证分布式事务回滚
        // if (true) {
        //     throw new RuntimeException("手动触发异常,测试分布式事务回滚");
        // }

        // 6. 远程调用:扣减用户账户余额(分支事务2)
        Boolean accountResult = accountFeignClient.deduct(userId, totalAmount);
        if (!accountResult) {
            throw new RuntimeException("余额扣减失败:用户账户余额不足");
        }
        log.info("【余额扣减成功】用户ID:{}, 扣减金额:{}", userId, totalAmount);

        // 7. 本地事务:更新订单状态为已支付
        order.setStatus(1);
        orderMapper.updateById(order);
        log.info("【订单状态更新完成】订单号:{}", orderNo);

        log.info("【全局事务执行完成】XID:{}", xid);
        return orderNo;
    }
}

核心注解必知必会:

  1. @GlobalTransactional(rollbackFor = Exception.class) 必须加rollbackFor,Spring 默认仅对 RuntimeException 和 Error 回滚,加此配置可覆盖所有异常,避免回滚失效。
  2. RootContext.getXID() 可获取当前全局事务唯一 ID,生产环境必须打印到日志中,出问题时可通过 XID 在 Seata 控制台、服务日志中全链路追踪。
  3. 分支执行失败必须抛出 RuntimeException,不能静默返回,否则 Seata 无法感知异常,不会触发全局回滚。
  1. 控制器 OrderController
java 复制代码
@RestController
@RequestMapping("/order")
public class OrderController {

    @Resource
    private OrderService orderService;

    @PostMapping("/create")
    public ResponseEntity<String> createOrder(
            @RequestParam Long userId,
            @RequestParam Long productId,
            @RequestParam Integer count,
            @RequestParam BigDecimal unitPrice) {
        try {
            String orderNo = orderService.createOrder(userId, productId, count, unitPrice);
            return ResponseEntity.ok("订单创建成功,订单号:" + orderNo);
        } catch (Exception e) {
            log.error("订单创建失败", e);
            return ResponseEntity.internalServerError().body("订单创建失败:" + e.getMessage());
        }
    }
}
  1. 启动类 OrderServiceApplication
java 复制代码
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.example.order.mapper")
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

6.4 AT 模式实战测试:一致性验证

6.4.1 环境启动顺序
  1. 启动 Nacos 服务,确认服务正常
  2. 启动 Seata Server 集群,确认已注册到 Nacos
  3. 依次启动 account-service、storage-service、order-service,确认所有服务均注册到 Nacos,且与 Seata Server 成功建立连接

启动成功后,可通过 Seata 控制台(http://TC 节点 IP:8091)查看注册的服务实例与事务分组。

6.4.2 正常流程测试

调用订单创建接口,传入正常参数:

bash 复制代码
POST http://localhost:8080/order/create
Content-Type: application/x-www-form-urlencoded

userId=1&productId=1&count=2&unitPrice=100

正常返回结果

bash 复制代码
订单创建成功,订单号:ORDER_1712345678901_1

数据一致性校验

  • 订单库:生成订单记录,status=1(已支付)
  • 库存库:商品 1 可用库存从 100→98,扣减 2 个
  • 账户库:用户 1 余额从 1000→800,扣减 200
  • 三个库的 undo_log 表:全局事务提交后,undo_log 已被异步删除,无残留记录
6.4.3 异常回滚测试(核心验证)

放开订单服务中手动抛出异常的代码,模拟库存扣减成功后、余额扣减前出现异常,重启订单服务,再次调用相同接口。

异常返回结果

bash 复制代码
订单创建失败:手动触发异常,测试分布式事务回滚

回滚效果校验(AT 模式核心能力验证)

  • 订单库:无新增订单记录,本地事务回滚成功
  • 库存库:商品 1 可用库存仍为 100,扣减操作已通过 undo_log 反向回滚
  • 账户库:用户 1 余额仍为 1000,无扣减操作
  • 三个库的 undo_log 表:回滚完成后,undo_log 已被清理

通过日志可完整看到回滚流程:异常触发后,TC 向所有已执行的分支下发回滚指令,RM 通过 undo_log 自动生成反向 SQL,完成数据回滚,全程无需业务代码介入,完美实现了分布式事务的原子性。

七、TCC 模式实战:高并发库存扣减场景全流程落地

AT 模式虽零侵入、开发成本低,但在高并发热点数据场景(如秒杀库存、金融账户)中,全局锁竞争会导致性能瓶颈,且无法支持 Redis、MongoDB 等非关系型数据库。而 TCC 模式完全由业务代码控制,无全局锁竞争,性能极强,可覆盖所有复杂业务场景,是大厂高并发核心链路的首选方案。

本次实战我们将库存扣减改造为 TCC 模式,实现AT+TCC 混合模式分布式事务(生产环境最常用方案:常规业务用 AT,热点核心链路用 TCC,兼顾开发效率与性能)。

7.1 前置准备:表结构改造

TCC 模式的核心是资源预留,我们需要改造库存表,新增冻结库存字段,用于 Try 阶段的资源锁定:

sql 复制代码
-- 库存表新增冻结库存字段
ALTER TABLE `t_storage` 
ADD COLUMN `frozen_count` int NOT NULL DEFAULT 0 COMMENT '冻结库存' AFTER `available_count`;

-- 重置测试数据
UPDATE `t_storage` SET available_count=100, frozen_count=0 WHERE product_id=1;

三个核心字段的职责划分:

  • available_count:可用库存,用户可下单的库存,Try 阶段扣减
  • frozen_count:冻结库存,Try 阶段锁定的资源,Confirm 阶段真正扣减、Cancel 阶段释放
  • total_count:总库存,Confirm 阶段最终扣减

同时,在库存库中创建tcc_fence_log表,用于 Seata 内置的 TCC Fence 能力,一键解决幂等、空回滚、悬挂三大问题:

sql 复制代码
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
    `xid`           VARCHAR(128)  NOT NULL COMMENT '全局事务ID',
    `branch_id`     BIGINT        NOT NULL COMMENT '分支事务ID',
    `action_name`   VARCHAR(64)   NOT NULL COMMENT 'TCC接口名称',
    `status`        TINYINT       NOT NULL COMMENT '状态:1=trying,2=committed,3=rollbacked,4=suspended',
    `gmt_create`    DATETIME(3)   NOT NULL COMMENT '创建时间',
    `gmt_modified`  DATETIME(3)   NOT NULL COMMENT '更新时间',
    PRIMARY KEY (`xid`, `branch_id`),
    KEY `idx_gmt_modified` (`gmt_modified`),
    KEY `idx_status` (`status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT='TCC事务防护表';

7.2 TCC 核心接口定义与实现

7.2.1 TCC 接口定义
java 复制代码
/**
 * 库存扣减TCC接口
 * 接口需保证全局唯一,二阶段方法名必须与注解中配置完全一致
 */
public interface StorageTccService {

    /**
     * Try阶段:资源检查与预留
     * @TwoPhaseBusinessAction 标记TCC两阶段接口
     * name:TCC接口全局唯一名称,不可重复
     * commitMethod:二阶段确认方法名,必须与实际方法名完全一致
     * rollbackMethod:二阶段回滚方法名,必须与实际方法名完全一致
     * useTCCFence=true:开启TCC防护,自动解决幂等、空回滚、悬挂三大问题(生产必开)
     * @BusinessActionContextParameter:标记参数,自动传递到二阶段上下文
     */
    @TwoPhaseBusinessAction(
            name = "storageTccService",
            commitMethod = "confirm",
            rollbackMethod = "cancel",
            useTCCFence = true
    )
    boolean tryFreezeStorage(BusinessActionContext context,
                             @BusinessActionContextParameter(paramName = "productId") Long productId,
                             @BusinessActionContextParameter(paramName = "count") Integer count);

    /**
     * Confirm阶段:确认提交,真正扣减冻结资源
     * 方法名必须与commitMethod一致,入参只能是BusinessActionContext
     */
    boolean confirm(BusinessActionContext context);

    /**
     * Cancel阶段:取消回滚,释放冻结资源
     * 方法名必须与rollbackMethod一致,入参只能是BusinessActionContext
     */
    boolean cancel(BusinessActionContext context);
}
7.2.2 TCC 接口实现

严格遵循 TCC 三阶段职责划分,严禁越界操作:

java 复制代码
@Slf4j
@Service
public class StorageTccServiceImpl implements StorageTccService {

    @Resource
    private StorageMapper storageMapper;

    /**
     * Try阶段:仅做资源检查+预留,不做真正的业务扣减
     * 核心:扣减可用库存,增加冻结库存,完成资源锁定
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean tryFreezeStorage(BusinessActionContext context, Long productId, Integer count) {
        String xid = context.getXid();
        log.info("【TCC Try阶段开始】XID:{}, 商品ID:{}, 冻结数量:{}", xid, productId, count);

        // 1. 参数校验
        if (productId == null || count == null || count <= 0) {
            throw new IllegalArgumentException("库存冻结参数异常");
        }

        // 2. 检查可用库存,执行资源冻结
        int rows = storageMapper.freezeStorage(productId, count);
        if (rows <= 0) {
            log.error("【TCC Try阶段失败】商品库存不足,XID:{}", xid);
            throw new RuntimeException("商品可用库存不足");
        }

        log.info("【TCC Try阶段完成】库存冻结成功,XID:{}", xid);
        return true;
    }

    /**
     * Confirm阶段:仅做确认提交,使用Try阶段预留的资源
     * 核心:扣减冻结库存,完成真正的业务扣减,必须保证幂等
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean confirm(BusinessActionContext context) {
        String xid = context.getXid();
        log.info("【TCC Confirm阶段开始】XID:{}", xid);

        // 从上下文获取Try阶段传递的参数
        Long productId = context.getActionContext("productId", Long.class);
        Integer count = context.getActionContext("count", Integer.class);

        // 确认扣减冻结库存
        int rows = storageMapper.confirmDeduct(productId, count);
        if (rows <= 0) {
            log.warn("【TCC Confirm阶段无数据更新】可能为重复执行,XID:{}", xid);
            return true; // 幂等处理,直接返回成功
        }

        log.info("【TCC Confirm阶段完成】库存扣减成功,XID:{}", xid);
        return true;
    }

    /**
     * Cancel阶段:仅做资源释放,回滚Try阶段的操作
     * 核心:释放冻结库存,恢复可用库存,必须处理空回滚、幂等、悬挂
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean cancel(BusinessActionContext context) {
        String xid = context.getXid();
        log.info("【TCC Cancel阶段开始】XID:{}", xid);

        // 从上下文获取Try阶段传递的参数
        Long productId = context.getActionContext("productId", Long.class);
        Integer count = context.getActionContext("count", Integer.class);

        // 释放冻结库存,恢复可用库存
        int rows = storageMapper.unfreezeStorage(productId, count);
        if (rows <= 0) {
            log.warn("【TCC Cancel阶段无数据更新】可能为空回滚或重复执行,XID:{}", xid);
            return true; // TCC Fence已处理空回滚和悬挂,直接返回成功
        }

        log.info("【TCC Cancel阶段完成】库存冻结已释放,XID:{}", xid);
        return true;
    }
}
7.2.3 Mapper 接口新增 SQL
java 复制代码
@Mapper
public interface StorageMapper extends BaseMapper<Storage> {
    // 原有扣减方法略...

    /**
     * Try阶段:冻结库存
     */
    @Update("UPDATE t_storage SET available_count = available_count - #{count}, frozen_count = frozen_count + #{count} " +
            "WHERE product_id = #{productId} AND available_count >= #{count}")
    int freezeStorage(@Param("productId") Long productId, @Param("count") Integer count);

    /**
     * Confirm阶段:确认扣减冻结库存
     */
    @Update("UPDATE t_storage SET frozen_count = frozen_count - #{count}, total_count = total_count - #{count} " +
            "WHERE product_id = #{productId} AND frozen_count >= #{count}")
    int confirmDeduct(@Param("productId") Long productId, @Param("count") Integer count);

    /**
     * Cancel阶段:释放冻结库存
     */
    @Update("UPDATE t_storage SET available_count = available_count + #{count}, frozen_count = frozen_count - #{count} " +
            "WHERE product_id = #{productId} AND frozen_count >= #{count}")
    int unfreezeStorage(@Param("productId") Long productId, @Param("count") Integer count);
}
7.2.4 控制器与 Feign 接口改造
  1. 库存服务 TCC 控制器
java 复制代码
@RestController
@RequestMapping("/storage/tcc")
public class StorageTccController {

    @Resource
    private StorageTccService storageTccService;

    @PostMapping("/try")
    public ResponseEntity<Boolean> tryFreeze(BusinessActionContext context,
                                              @RequestParam Long productId,
                                              @RequestParam Integer count) {
        boolean result = storageTccService.tryFreezeStorage(context, productId, count);
        return ResponseEntity.ok(result);
    }
}
  1. 订单服务 TCC Feign 接口
java 复制代码
@FeignClient(value = "storage-service", fallbackFactory = StorageTccFeignFallbackFactory.class)
public interface StorageTccFeignClient {

    @PostMapping("/storage/tcc/try")
    Boolean tryFreezeStorage(@RequestBody BusinessActionContext context,
                             @RequestParam("productId") Long productId,
                             @RequestParam("count") Integer count);
}

@Component
public class StorageTccFeignFallbackFactory implements FallbackFactory<StorageTccFeignClient> {
    @Override
    public StorageTccFeignClient create(Throwable cause) {
        return (context, productId, count) -> {
            throw new RuntimeException("库存TCC服务调用异常:" + cause.getMessage(), cause);
        };
    }
}
7.2.5 订单服务改造:AT+TCC 混合模式
java 复制代码
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    @Resource
    private OrderMapper orderMapper;
    @Resource
    private StorageTccFeignClient storageTccFeignClient;
    @Resource
    private AccountFeignClient accountFeignClient;

    @Override
    @GlobalTransactional(name = "order-create-tcc-tx", rollbackFor = Exception.class)
    public String createOrder(Long userId, Long productId, Integer count, BigDecimal unitPrice) {
        String xid = RootContext.getXID();
        log.info("【混合模式全局事务开启】XID:{}", xid);

        String orderNo = "ORDER_TCC_" + System.currentTimeMillis() + "_" + userId;
        BigDecimal totalAmount = unitPrice.multiply(new BigDecimal(count));

        // AT模式:本地创建订单
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setProductId(productId);
        order.setCount(count);
        order.setAmount(totalAmount);
        order.setStatus(0);
        orderMapper.insert(order);
        log.info("【订单创建成功】订单号:{}", orderNo);

        // TCC模式:冻结库存(热点链路)
        BusinessActionContext context = new BusinessActionContext();
        context.setXid(xid); // 传递全局XID
        Boolean tryResult = storageTccFeignClient.tryFreezeStorage(context, productId, count);
        if (!tryResult) {
            throw new RuntimeException("库存冻结失败:商品库存不足");
        }
        log.info("【TCC Try阶段完成】库存冻结成功");

        // 回滚测试点
        // if (true) {
        //     throw new RuntimeException("手动触发异常,测试TCC混合模式回滚");
        // }

        // AT模式:扣减账户余额
        Boolean accountResult = accountFeignClient.deduct(userId, totalAmount);
        if (!accountResult) {
            throw new RuntimeException("余额扣减失败:账户余额不足");
        }
        log.info("【余额扣减成功】用户ID:{}", userId);

        // 更新订单状态
        order.setStatus(1);
        orderMapper.updateById(order);
        log.info("【订单状态更新完成】订单号:{}", orderNo);

        log.info("【混合模式全局事务完成】XID:{}", xid);
        return orderNo;
    }
}

核心优势:Seata 完美支持 AT+TCC 混合模式,同一个全局事务中,常规业务用 AT 零侵入开发,热点核心链路用 TCC 高性能实现,兼顾开发效率与并发性能,是生产环境的首选方案。

7.3 TCC 模式实战测试

7.3.1 正常流程测试

调用订单接口,正常参数执行后,数据校验结果:

  • 订单库:订单创建成功,status=1
  • 库存库:可用库存 98,冻结库存 0,总库存 98(Try 冻结的库存已在 Confirm 阶段真正扣减)
  • 账户库:余额 800,扣减成功
  • tcc_fence_log 表:生成 status=2(committed)的记录,标记 TCC 事务已提交
7.3.2 异常回滚测试

放开手动异常代码,触发全局回滚后,数据校验结果:

  • 订单库:无新增订单,AT 分支回滚成功
  • 库存库:可用库存 100,冻结库存 0,总库存 100(Try 冻结的库存已在 Cancel 阶段释放)
  • 账户库:余额 1000,无扣减
  • tcc_fence_log 表:生成 status=3(rollbacked)的记录,标记 TCC 事务已回滚

TCC 模式回滚完美生效,且 Seata 的 TCC Fence 自动解决了幂等、空回滚、悬挂三大问题,无需手动开发复杂的防护逻辑。

八、生产级避坑指南:90% 开发者都会踩的 Seata 坑,全解全破

这部分是本文的核心价值所在,是我 5 年微服务架构、双 11 核心链路踩坑沉淀的生产经验,覆盖 AT/TCC 模式、环境部署、性能优化的全场景坑点,每一个都有可落地的解决方案,帮你避开 90% 的生产事故。

8.1 AT 模式专属避坑指南

坑 1:AT 模式不回滚,90% 开发者都踩过的终极坑

根本原因

  1. 数据源未被 Seata 代理,无法拦截 SQL 生成 undo_log
  2. @GlobalTransactional未加rollbackFor = Exception.class,非 RuntimeException 异常不触发回滚
  3. 业务代码捕获异常后未重新抛出,Seata 感知不到异常
  4. Feign 降级返回默认值,未抛出异常,Seata 判定分支执行成功
  5. 业务库未创建 undo_log 表,或表结构与官方不一致
  6. 客户端tx-service-group与服务端vgroupMapping配置不匹配,无法连接 TC

解决方案

  1. SpringBoot 3.0 + 严禁手动配置 DataSourceProxy,避免双重代理导致代理失效;多数据源场景必须将动态数据源交给 Seata 代理
  2. 必须给@GlobalTransactional加上rollbackFor = Exception.class,覆盖所有异常场景
  3. 业务捕获异常后,必须抛出 RuntimeException,严禁静默处理
  4. Feign 降级逻辑必须抛出异常,不能返回 null/false
  5. 所有业务库必须创建官方标准的 undo_log 表,不可修改核心字段
  6. 客户端tx-service-group必须与服务端service.vgroupMapping.xxx=default中的 xxx 完全一致
坑 2:AT 模式脏写,生产致命数据不一致事故

根本原因:非 Seata 代理的数据源(如 Navicat、DMS、原生 JDBC)修改了业务表,绕过了全局锁检查,导致 Seata 回滚时前后镜像不一致,回滚失败,出现脏写。

解决方案

  1. 生产环境严格管控业务表的修改权限,所有操作必须通过 Seata 代理的数据源执行,严禁手动修改业务表数据
  2. 开启脏写校验:seata.client.rm.undo.log.validation.enable=true,回滚时自动校验数据是否被篡改
  3. 核心业务表增加乐观锁 version 字段,避免并发修改
  4. 配置脏写告警,校验失败时立即触发人工介入
坑 3:热点数据场景全局锁竞争激烈,TPS 上不去

根本原因:AT 模式下,热点数据的全局锁被持有期间,其他事务必须等待,高并发下出现大量锁等待超时,TPS 瓶颈明显。

解决方案

  1. 热点数据场景优先切换为 TCC 模式,无全局锁竞争,性能提升 10 倍以上
  2. 优化锁参数:seata.client.rm.lock-retry-times=30seata.client.rm.lock-retry-interval=100,根据业务调整重试策略
  3. 拆分热点数据,如将单个商品库存拆分为多个分片,减少锁竞争
  4. 缩短全局事务执行时长,非事务操作放到全局事务之外,减少锁持有时间
坑 4:undo_log 表持续膨胀,数据库性能下降

根本原因:TC / 服务宕机导致 undo_log 未被异步清理,日积月累表数据量过大,影响数据库性能。

解决方案

  1. 开启自动清理:seata.client.rm.undo.log.clean-period=86400000,每天自动清理 7 天前的历史数据
  2. 手动创建定时任务,定期清理log_status=0log_created超过 7 天的记录
  3. 生产环境对 undo_log 表按时间做分区表,方便历史数据清理

8.2 TCC 模式专属避坑指南

坑 1:幂等、空回滚、悬挂三大问题处理不当,数据不一致

根本原因:网络抖动、服务超时导致 Cancel 比 Try 先执行,或二阶段方法被重复调用,未做防护导致数据重复扣减、资源永久冻结。

解决方案

  1. 生产环境必须开启useTCCFence = true,Seata 自动解决三大问题,无需手动开发
  2. 低版本 Seata 必须手动实现 TCC 事务控制表,记录分支事务执行状态,执行前先校验状态
  3. Try/Confirm/Cancel 三个方法必须保证幂等性,严禁重复执行导致数据异常
坑 2:二阶段方法找不到,事务超时回滚失败

根本原因@TwoPhaseBusinessAction中的commitMethod/rollbackMethod与实际方法名不一致,或方法参数不符合规范,Seata 无法调用二阶段方法。

解决方案

  1. 二阶段方法名必须与注解配置完全一致,包括大小写
  2. Confirm/Cancel 方法的入参只能是BusinessActionContext,不能有其他参数
  3. Try 方法的参数必须加@BusinessActionContextParameter注解,否则二阶段无法获取参数
  4. TCC 接口的 name 必须全局唯一,不可与其他接口重名
坑 3:Try 阶段直接执行业务扣减,未做资源预留,回滚失效

根本原因:开发者在 Try 阶段直接扣减可用资源,未做冻结预留,导致 Cancel 阶段无法回滚,数据不一致。

解决方案

  1. 严格遵循 TCC 设计原则:Try 仅做资源检查 + 预留,Confirm 才做真正的业务扣减,Cancel 释放预留资源
  2. 必须通过冻结字段实现资源预留,严禁直接扣减可用资源
坑 4:二阶段执行失败,事务悬挂数据永久不一致

根本原因:Confirm/Cancel 执行失败,Seata 重试多次仍失败,导致事务悬挂,资源永久冻结。

解决方案

  1. 配置二阶段重试次数:seata.client.tm.commit-retry-count=5seata.client.tm.rollback-retry-count=5
  2. 生产环境配置事务悬挂告警,二阶段重试失败立即人工介入
  3. Confirm/Cancel 方法必须保证幂等性,支持重复执行
  4. 核心业务开发人工补偿接口,自动重试失败时可手动干预

8.3 通用避坑指南(AT/TCC 通用)

坑 1:版本不兼容,服务启动失败、事务不生效

根本原因:Seata 客户端与服务端大版本不一致,SpringCloud Alibaba 与 Seata 版本不匹配,导致类冲突、协议不兼容。

解决方案

  1. 严格遵守版本对应关系,客户端与服务端大版本必须完全一致
  2. 排除 SpringCloud Alibaba 内置的 Seata 依赖,手动引入与服务端版本一致的依赖
  3. 生产环境严禁使用 SNAPSHOT 版本,必须使用 RELEASE 稳定版
坑 2:TC 单点故障,全系统分布式事务瘫痪

根本原因:生产环境仅部署单节点 TC,节点宕机后所有分布式事务无法执行,系统瘫痪。

解决方案

  1. 生产环境必须部署 TC 集群,至少 3 个节点,跨机房部署
  2. 使用 Nacos 等注册中心实现 TC 服务发现,客户端自动感知节点上下线
  3. TC 存储使用 MySQL 主从 / MGR 集群,避免数据库单点
  4. 配置 TC 节点健康检查与宕机告警
坑 3:XID 传递失败,分支事务无法加入全局事务

根本原因:服务间远程调用未传递 XID,下游分支事务无法加入全局事务,导致回滚失效。

解决方案

  1. Seata 已内置 Feign/Dubbo 等主流 RPC 框架的 XID 自动传递机制,无需手动处理
  2. 自定义 HTTP 调用需手动将 XID 放入请求头,下游通过拦截器设置到RootContext
  3. 生产环境打印每个分支的 XID,排查传递失败问题

九、生产级性能优化与高可用最佳实践

9.1 核心性能优化手段

  1. 模式选型优化:热点链路用 TCC,常规业务用 AT,避免 AT 模式全局锁竞争
  2. 事务时长优化:缩短全局事务执行时间,非事务操作放到全局事务之外,减少锁持有时间
  3. 参数调优
    • 客户端线程池调优:seata.client.rm.thread-pool.core-pool-size=20max-pool-size=100
    • 开启异步提交:seata.client.rm.async-commit-buffer-limit=1000,提升 AT 模式提交性能
    • 调整全局锁重试参数,减少无效重试的 CPU 消耗
  4. 数据库优化
    • undo_log、tcc_fence_log 表必须创建正确的索引,避免慢 SQL
    • 大表按时间分区,定期清理历史数据
    • 数据库采用主从架构、读写分离,提升性能
  5. 批量操作优化:避免全局事务中循环执行单条 SQL,优先使用批量操作,减少数据库交互次数

9.2 高可用最佳实践

  1. TC 集群高可用:至少 3 节点跨机房部署,避免单机房故障
  2. 注册 / 配置中心高可用:Nacos 集群至少 3 节点,保证服务发现能力稳定
  3. 存储高可用:TC 数据库采用 MySQL MGR 集群,避免存储单点故障
  4. 全链路监控告警
    • 监控 TC 集群节点健康状态、CPU、内存、磁盘 IO
    • 监控全局事务成功率、回滚率、平均执行时长
    • 监控二阶段重试失败、悬挂事务,立即触发告警
    • 监控 undo_log、tcc_fence_log 表数据量
  5. 容灾降级方案
    • 核心业务开发降级开关,Seata 集群故障时可降级为本地事务 + 最终一致性方案
    • 制定应急预案,TC 集群全宕机时的快速恢复流程
    • 定期开展混沌工程演练,模拟 TC 宕机、网络分区等故障,验证系统容灾能力

十、总结

本文从分布式事务的核心痛点出发,深入拆解了 Seata 的核心架构、AT/TCC 模式的底层原理,从环境搭建、全流程实战到生产级避坑指南、高可用最佳实践,完整覆盖了 Seata 从入门到生产落地的全链路内容。

核心要点总结:

  1. Seata 三大核心组件 TC/TM/RM 是所有模式的基础,TC 是全局事务大脑,TM 是事务发起者,RM 是分支执行者
  2. AT 模式零侵入、开发成本低,适合 90% 的常规业务场景,核心是 undo_log 自动回滚机制 + 全局锁隔离机制
  3. TCC 模式高性能、高灵活,适合高并发热点场景与异构数据源场景,核心是 Try-Confirm-Cancel 三阶段设计,必须处理好三大经典问题
  4. AT+TCC 混合模式是生产环境首选,兼顾开发效率与并发性能
  5. 生产环境必须严格遵守避坑指南,避免数据不一致的致命事故
  6. 生产环境必须部署 TC 集群,建立完善的监控告警与容灾方案

结语

如果本文对你有帮助,欢迎点赞、收藏、关注三连,有任何关于 Seata 分布式事务的问题,欢迎在评论区留言,我会一一回复。

如果你在生产环境落地 Seata 的过程中遇到了疑难问题,也可以私信我,我会提供针对性的解决方案。

原创不易,转载请注明出处,感谢大家的支持!

相关推荐
ℳ₯㎕ddzོꦿ࿐2 小时前
实战篇:结合 GitLab CI/CD 实现 Spring Cloud 微服务自动化部署与防坑指南
spring cloud·ci/cd·gitlab
喜欢流萤吖~3 小时前
分布式事务:微服务的数据一致性之困
分布式·微服务·架构
user_admin_god19 小时前
SSE 流式响应 Chunk 被截断问题的排查与修复
java·人工智能·spring boot·spring·maven·mybatis
大G的笔记本20 小时前
分布式事务
分布式
weixin_4196583120 小时前
RabbitMQ 的高级特性
java·分布式·rabbitmq
_F_y21 小时前
仿RabbitMQ实现消息队列-服务端核心模块实现(1)
分布式·rabbitmq
TE-茶叶蛋21 小时前
Spring最核心扩展点:BeanPostProcessor
java·后端·spring
大龄码农-涵哥1 天前
Spring Boot项目集成AI对话:使用Spring AI打造智能客服
人工智能·spring boot·spring
Full Stack Developme1 天前
MyBatis-Plus 注解教程
java·spring·mybatis