Skip to content

分布式事务

  分布式事务是微服务架构中最核心的难题之一。当一次业务操作跨越多个服务进程,触发多个彼此隔离的本地事务时,如何保证数据在全局范围内的原子性和一致性?

  需要特别澄清的是,这里的“分布式”并不等同于“分库分表”。即使多个微服务共享同一个物理数据库,只要每个服务通过独立的数据库连接(Session)管理自身的本地事务,跨服务的 RPC 调用依然无法依赖单机的 @Transactional 来保证原子性。单体架构中的“本地事务边界”在微服务进程间是天然失效的。

  归根结底,进程边界带来的事务上下文隔离,以及网络通信固有的超时与分区风险,共同构成了分布式事务要解决的根本矛盾——这也是分布式事务远比单机事务复杂的根源所在。

一、从单机事务到分布式事务

1.1 单机事务的局限

  在的部署架构下,所有业务数据均存放在同一个数据库实例中,此时事务管理非常简单:

单机事务(一个数据库):

  @Transactional
  public void createOrder() {
      orderMapper.insert(order);      // 写订单表
      inventoryMapper.deduct(1001);   // 扣库存
      accountMapper.deduct(100);      // 扣余额
  }
  // 要么全成功,要么全回滚,一个 @Transactional 搞定

  单机事务依赖数据库的 ACID 特性,由数据库自身保证原子性。

  但当把系统数据拆分为多个数据库实例时,单机事务无法满足需求。

分布式场景1(单机 + 多数据源):

同一个 Application 进程
   ├── 数据源1(订单库)→ INSERT 订单 ✅
   ├── 数据源2(库存库)→ 扣减库存 ✅
   └── 数据源3(账户库)→ 扣减余额 ❌ 失败

  怎么回滚?@Transactional 默认绑定至单一数据源的事务管理器,
  无法协调多个独立数据库连接(Connection)之间的原子性。

  同样的,当系统拆分为微服务后,数据分散在不同的数据库中:

分布式场景2(多服务 + 多数据源):

  订单服务 ──→ 订单数据库   ──→ INSERT 订单 ✅
  库存服务 ──→ 库存数据库   ──→ 扣减库存 ✅
  账户服务 ──→ 账户数据库   ──→ 扣减余额 ❌ 失败!

  怎么回滚?每个微服务持有独立的数据库连接(Session),各自管理自身的本地事务。
  @Transactional 无法跨越多个服务进程和多个事务管理器来协调原子性。

  核心结论: 无论是单机多数据源,还是微服务多进程,@Transactional 的生效前提都是 “单一本地事务边界”。一旦一次业务操作涉及 两个或以上的独立事务资源(Transaction Resources),本地事务的 ACID 特性便立即失效——这正是分布式事务要解决的根本矛盾。

1.2 本质矛盾

  分布式事务的本质矛盾在于:事务协调范围的扩大与 单点控制力的丧失 之间的根本冲突。

  在单机单库场景下,应用层仅需面对 “一个”数据库连接和“一个”本地事务。所有的 SQL 操作都在同一个事务上下文中,由数据库自身的 ACID 机制统一管辖,原子性由底层存储引擎的锁与日志机制自然保证。 然而,一旦系统引入 多个独立的事务资源(Transaction Resources)——无论这些资源是同一个数据库的多个连接(Session)、多个物理数据库,还是消息队列(MQ)——原本“单点控制”的局面便被彻底打破:

  本地事务边界相互隔离: 每个数据源(或每个微服务)各自管理自身的本地事务,彼此之间无法共享 Undo 日志,也无法感知对方的事务状态。

  缺乏全局协调者: 没有一个内置的“全局事务管理器”来统筹所有参与者的最终决策(全部提交或全部回滚)。

  网络带来的不确定性: 跨进程或跨数据源的通信引入网络延迟、超时和分区风险,使得“同步等待”和“故障判定”成为极度复杂的工程难题。

  核心结论: 分布式事务要解决的,本质上就是 “如何在缺乏单一控制中心的情况下,通过跨网络的协调协议,将多个孤立的本地事务捆绑成一个具备原子性的逻辑工作单元。”

单机事务 vs 分布式事务:

  单机事务:
    ✅ 一个数据库 → 一个事务管理器 → 原子性自然保证
    ✅ 本地锁 → 隔离性自然保证
    ✅ redo/undo log → 持久性自然保证

  分布式事务:
    ❌ 多个数据库 → 没有全局事务管理器
    ❌ 跨网络 → 网络不可靠(延迟、超时、分区)
    ❌ 各服务独立 → 部分成功部分失败是常态
    ❌ 没有全局锁 → 隔离性难以保证

1.3 典型场景

场景涉及服务事务要求
下单订单 + 库存 + 账户 + 优惠券扣库存、扣余额、减优惠券、创建订单,全部成功或全部回滚
转账账户 A + 账户 BA 扣钱 + B 加钱,要么都成功,要么都不做
秒杀库存 + 订单 + 消息队列扣库存成功后,订单创建和消息发送也必须成功
退款订单 + 账户 + 库存订单状态更新、余额退回、库存恢复

二、CAP 理论——分布式系统的天花板

2.1 什么是 CAP

  CAP 定理是分布式系统的基石,由 Eric Brewer 在 2000 年提出:一个分布式系统最多只能同时满足以下三项中的两项。

CAP 三角:

            C(一致性)
            /\
           /  \
          /    \
         /      \
        /   ❌   \
       / 不可兼得  \
      /____________\
  A(可用性)      P(分区容错性)
特性含义精确解释例子
C(Consistency)所有节点同一时间看到相同的数据强一致性(线性一致性):写入后,所有副本必须同步完成,后续读请求必须返回最新值写入订单库主库后,读从库也必须立刻读到该订单(若读不到,系统就违反了 C)
A(Availability)每次请求都能获得非错误的响应高可用性:集群中非故障节点必须在有限时间内响应,不能因等待同步而无限阻塞即使库存节点间网络延迟,查询接口也不能报 5xx 错误,必须返回数据(哪怕是旧值)
P(Partition Tolerance)网络分区时系统仍能工作容忍节点间通信中断:节点间丢包、超时或断连时,系统整体功能不能完全瘫痪订单服务与库存服务之间的 RPC 超时(网络中断),订单不能被“卡死”,必须做出决策

  在微服务 / 分库场景下,网络分区(P)的具体表现:

  **服务间 RPC 调用超时(对方没响应,无法判断是“挂了”还是“慢”)

  **数据库主从同步延迟过大(主库已写,从库未同步,形成“时间轴上的分区”)

  **机房光缆中断导致跨机房节点失联

  核心决策前提: 由于网络分区(P)在分布式系统中是无法避免的客观物理现实,架构师的实际选择空间并不在“三选二”,而在于 当 P 发生时,牺牲一致性(保 AP)还是牺牲可用性(保 CP)。这一点,将在下一节深入探讨。

  关键误区澄清:

2.2 为什么 P 必须选择

  在分布式系统中,P(分区容错性)是不可协商的必选项。原因在于,网络故障是客观物理规律,无法消除:

分布式系统中,P(分区容错性)是必须的:

  网络分区(P)的常见诱因:
  - 交换机故障 / 网线断开
  - 网络拥塞导致 RPC 超时
  - 服务节点 Full GC 导致心跳丢失(被注册中心剔除)
  - 跨机房 / 跨地域部署的光缆中断

  既然 P 无法避免,架构师只能在 C(强一致性) 和 A(高可用性) 之间做出取舍:

  CP 系统(牺牲可用性,保住一致性): 分区发生时,系统暂停写入(或返回错误),宁可短暂不可用,也绝不返回错误的数据。恢复后,所有节点强制同步,确保数据高度一致。典型代表:ZooKeeper、Seata TC、Etcd。

  AP 系统(牺牲一致性,保住可用性): 分区发生时,系统持续提供服务,各节点独立响应请求,但可能会返回过期的数据(脏读)。网络恢复后,节点间通过异步复制追赶数据,达到最终一致。典型代表:Eureka、Nacos AP 模式、Cassandra。

  关键澄清:

2.3 CP vs AP 的选择

2.3.1 CP 系统(强一致,牺牲部分可用性)

适用场景: 对数据一致性要求极高的场景(金融、交易、库存精确扣减)。

代表产品: ZooKeeper、Etcd、Consul、Seata TC。

工作原理: 写入数据 → 协调者发起提议 → 等待超过半数(多数派)节点确认写入 → 返回成功。若网络分区导致达不到多数派,集群自动进入“只读模式”或拒绝服务,宁可系统暂时不可用,也绝不向客户端返回错误或未同步的数据。

银行转账场景(适合 CP): 张三给李四转 100 元 → 必须保证张三扣了钱、李四加了钱。宁可暂时不可用(报错提示系统繁忙),也不能出现“钱扣了但没到账”的状态。

2.3.2 AP 系统(高可用,牺牲强一致)

适用场景: 对可用性要求极高,能容忍短暂不一致(读多写少、非资金类)。

代表产品: Eureka、Nacos(AP 模式)、Cassandra。

工作原理: 写入数据 → 主节点确认即返回成功 → 后台异步同步到其他节点。网络分区时,各节点继续独立响应请求,恢复后通过 Gossip 协议合并数据。此时客户端请求返回的是旧值(过期数据),但绝不会捏造“不存在的数据”。

商品浏览场景(适合 AP): 用户浏览商品列表 → 短暂显示库存“还剩 100 件”(实际已经卖了 10 件)没关系。宁可数据略有延迟,也不能让用户看到报错页面导致跳出。

2.3.3 架构师终极决策矩阵(系统混合部署)

  在实际微服务架构中,不存在“选 CP 就全 CP,选 AP 就全 AP”。正确的策略是按业务分而治之:

  订单/账户/库存(写强一致) → 必须用 CP(配合 Seata TCC/AT),保证数据精准。

  商品详情/用户评论/日志(读多写少) → 大胆用 AP(配合缓存与 MQ 异步同步),保证用户体验。

  服务注册中心:服务发现(AP,如 Eureka)与服务配置(CP,如 ZooKeeper)可以共存。

2.4 CAP 的六大误区(生产级避坑指南)

  误区一(经典):“CAP 三者只能选两个”
  ✅ 纠正:由于 P(分区)在分布式系统中无法避免,实际场景下P 是必选项,架构师真正的决策空间只有 “C 和 A 二选一”。

   误区二(极端化):“选 CP 就完全不可用,选 AP 就完全不保证一致”
  ✅ 纠正:这是取舍,不是二选一的死局。CP 系统仅在网络分区时牺牲可用性,正常运行期间完全保证高并发读写;AP 系统仅在网络分区时牺牲一致性,正常运行时数据也是实时同步的(通过异步复制)。

  误区三(理论覆盖):“CAP 是分布式事务的唯一理论依据”
  ✅ 纠正:CAP 定理仅描述了分布式存储系统的数据同步特性,但并未给出“如果数据不一致了,后续怎么补救”的工程路径。因此,分布式事务还需要 BASE 理论(基本可用、软状态、最终一致性)来指导具体的代码实现和运维兜底。

  误区四(物理谬误):“网络分区(P)就是网线断了或机房炸了”
  ✅ 纠正:在微服务 Java 应用中,GC 停顿、线程池爆满、网络拥塞超时都会导致节点间通信失败,这同样是 P 的表现形式。因此,P 是常态,不是意外,架构设计必须每时每刻考虑分区容忍策略。

  误区五(事务耦合):“选了 AP(高可用),就不用管分布式事务了”
  ✅ 纠正:选 AP 只是放弃了同步强一致(如 Seata AT/2PC),但必须引入异步最终一致性方案(如 Saga、可靠消息、本地消息表)。否则,部分成功的数据会在数据库里永久沉淀,最终只能靠人工对账擦屁股。

  误区六(读写混淆):“数据库本身开了主从同步,业务代码就不管一致性了”
  ✅ 纠正:MySQL 主从同步默认是 AP 行为(异步)。如果业务要求“写后立即读”(如支付回调查详情),代码层必须通过数据源路由(强制读主库)或特定的 SQL 注解来绕过从库延迟,否则用户会看到刚付完款的订单显示“未支付”,引发严重客诉。

三、BASE 理论——分布式事务的指导思想

3.1 从 CAP 到 BASE

  CAP 告诉我们"做不到完美",BASE 告诉我们"做不到完美怎么办"。

CAP → BASE 的演进:

  CAP 说:分布式系统做不到完美的强一致 + 高可用
  BASE 说:那就退一步,追求最终一致性

  大部分互联网业务不需要强一致——
  用户支付成功后,订单状态晚 1 秒变成"已支付",完全可以接受

3.2 BASE 三要素

BA(Basically Available,基本可用):

  系统出现故障时,允许损失部分可用性,但保证核心功能可用

  例子:
    双十一期间,下单页面正常,但优惠券查询暂时不可用
    → 用户还是能下单,只是看不到优惠券
    → 核心功能(下单)可用,非核心功能(优惠券)暂时降级


S(Soft State,软状态):

  允许系统中的数据存在中间状态,该状态不影响系统整体可用性

  例子:
    订单状态为"支付中"(中间状态)
    → 这不是最终状态,但用户可以接受
    → 最终会变为"已支付"或"已取消"


E(Eventually Consistent,最终一致性):

  不要求数据实时一致,但保证最终会达到一致

  例子:
    支付成功后,账户余额可能 1 秒后才更新
    → 短时间内可能不一致
    → 但最终一定会正确
    → 用户刷新页面就能看到最新余额

3.3 强一致 vs 最终一致

维度强一致性最终一致性
写入后立刻读一定能读到最新值可能读到旧值
性能低(需要同步等待)高(异步处理)
可用性低(网络分区时不可用)高(各节点继续服务)
实现成本
适用场景银行转账、库存扣减用户信息、商品描述、统计数据
代表方案2PC、TCCSaga、可靠消息、AT 模式

四、分布式事务六大方案全景

4.1 方案总览

方案一致性性能复杂度代码侵入适用场景
2PC(XA)强一致⭐⭐⭐⭐传统单体拆微服务,跨库事务
TCC强一致⭐⭐⭐⭐⭐⭐⭐⭐资金、交易等核心业务
Saga最终一致⭐⭐⭐⭐⭐⭐⭐长流程、非资金类业务
AT(Seata)最终一致⭐⭐⭐⭐⭐对业务侵入小的场景
可靠消息最终一致⭐⭐⭐⭐⭐⭐⭐异步解耦、通知类场景
最大努力通知最终一致⭐⭐⭐⭐⭐⭐⭐外部系统回调、通知

4.2 方案选择决策树

你的业务场景                              → 推荐方案
─────────────────────────────────────────────────────────────
强一致性、资金交易(转账、支付)            → TCC
强一致性、简单跨库事务(单体拆微服务)       → 2PC(XA)
长流程、非资金类(订单创建→审核→发货)      → Saga
无侵入、快速接入、非强一致                 → AT(Seata)
异步解耦、通知类(短信、邮件)              → 可靠消息
外部系统回调(支付回调、物流通知)           → 最大努力通知

五、2PC(两阶段提交)—— 强一致性的经典方案

5.1 核心思想

  2PC 的核心思想:引入一个协调者,通过"先询问、再执行"两个阶段,保证所有参与者要么全部提交,要么全部回滚。

2PC = 协调者(Coordinator) + 参与者(Participant),分两个阶段

  阶段一:投票(Prepare / Voting Phase)
    协调者 → 所有参与者:你们能执行吗?
    参与者 → 协调者:我能(YES,锁定资源)/ 我不能(NO)

  阶段二:提交(Commit / Decision Phase)
    如果所有参与者都回复 YES:
      协调者 → 所有参与者:提交!
    如果任意参与者回复 NO:
      协调者 → 所有参与者:回滚!

5.2 正常流程

  协调者                          参与者 A          参与者 B
    │                               │                 │
    │──── Prepare ─────────────────→│                 │
    │──── Prepare ──────────────────────────────────→│
    │                               │                 │
    │←─── YES(锁定资源)──────────│                 │
    │←─── YES(锁定资源)──────────────────────────│
    │                               │                 │
    │──── Commit ──────────────────→│                 │
    │──── Commit ──────────────────────────────────→│
    │                               │                 │
    │←─── ACK ─────────────────────│                 │
    │←─── ACK ─────────────────────────────────────│
    │                               │                 │
    ✅ 事务提交成功

5.3 异常流程

参与者投票 NO 的情况:

  协调者                          参与者 A          参与者 B
    │                               │                 │
    │──── Prepare ─────────────────→│                 │
    │──── Prepare ──────────────────────────────────→│
    │                               │                 │
    │←─── YES ─────────────────────│                 │
    │←─── NO(资源不足)────────────────────────────│
    │                               │                 │
    │──── Rollback ────────────────→│  ← 通知 A 回滚  │
    │                               │                 │
    ✅ 所有参与者回滚,数据一致

5.4 2PC 的致命缺陷

❌ 同步阻塞:
  Prepare 阶段,参与者锁定资源,必须等协调者通知
  如果协调者挂了,参与者一直阻塞,资源无法释放
  → 这是 2PC 性能差的主要原因

❌ 单点故障:
  协调者挂了,整个系统瘫痪
  参与者不知道应该提交还是回滚
  → 需要引入协调者的高可用方案(增加了复杂度)

❌ 数据不一致(最致命):
  阶段二,协调者发了 Commit 后挂了
  有的参与者收到 Commit 提交了,有的没收到,数据不一致!
  → 2PC 没有彻底解决一致性问题,只是"大概率一致"

❌ 性能差:
  所有参与者都要锁定资源等协调者
  一次事务 = 2 次网络往返 × N 个参与者
  → 不适合高并发场景

5.5 3PC 的改进(了解即可)

3PC(三阶段提交)在 2PC 基础上增加了超时机制和预提交阶段:

  阶段一:CanCommit(询问)
  阶段二:PreCommit(预提交,这里才锁定资源)
  阶段三:DoCommit(提交)

  改进点:
    ✅ 增加了超时机制,参与者不会无限等待
    ✅ 预提交阶段后,参与者可以自主决定提交/回滚

  但 3PC 仍然无法解决网络分区时的数据不一致问题
  → 实际生产中使用极少,了解即可

六、TCC(Try-Confirm-Cancel)—— 业务层的强一致性

6.1 核心思想

  TCC 的核心思想:把一次业务操作拆成三个步骤,由业务代码自己控制资源的预留、确认和取消。

TCC 三阶段:

  Try(尝试):预留资源,检查 + 锁定
    → 冻结库存 100 件
    → 冻结余额 100 元
    → 此时资源还没真正扣减,只是"锁定"

  Confirm(确认):确认提交,使用预留的资源
    → 扣减冻结的库存
    → 扣减冻结的余额
    → 真正完成业务操作

  Cancel(取消):取消操作,释放预留的资源
    → 恢复冻结的库存
    → 恢复冻结的余额
    → 回到 Try 之前的状态

6.2 与 2PC 的关键区别

2PC 的问题:
  数据库层锁定资源 → 锁定时间长 → 性能差
  TCC 的改进:
  业务层预留资源 → 锁定时间短 → 性能好

  关键区别:
    - 2PC 的锁是数据库级的(行锁、表锁),由数据库管理
    - TCC 的锁是业务级的(冻结字段),由业务代码管理
    - TCC 的 Try 阶段完成后,资源已"预留"但数据库锁已释放

6.3 完整流程

TCC 正常流程:

  业务方(TM)                   库存服务                    账户服务
      │                            │                          │
      │──── Try ──────────────────→│                          │
      │  freezeStock(1001, 10)     │ 冻结 10 件库存             │
      │←─── OK ───────────────────│                          │
      │                            │                          │
      │──── Try ────────────────────────────────────────────→│
      │  freezeBalance(1001, 100)  │                          │ 冻结 100 元
      │←─── OK ─────────────────────────────────────────────│
      │                            │                          │
      │──── Confirm ──────────────→│                          │
      │  confirmFreeze(1001, 10)   │ 真正扣减库存              │
      │←─── OK ───────────────────│                          │
      │                            │                          │
      │──── Confirm ─────────────────────────────────────────→│
      │  confirmFreeze(1001, 100)  │                          │ 真正扣减余额
      │←─── OK ─────────────────────────────────────────────│
      │                            │                          │
      ✅ 事务完成


TCC 异常流程(Try 失败 → Cancel):

  业务方(TM)                   库存服务                    账户服务
      │                            │                          │
      │──── Try ──────────────────→│                          │
      │  freezeStock(1001, 10)     │ 冻结 10 件库存             │
      │←─── OK ───────────────────│                          │
      │                            │                          │
      │──── Try ────────────────────────────────────────────→│
      │  freezeBalance(1001, 100)  │                          │ 余额不足!
      │←─── FAIL ───────────────────────────────────────────│
      │                            │                          │
      │──── Cancel ───────────────→│                          │
      │  cancelFreeze(1001, 10)    │ 恢复 10 件库存             │
      │←─── OK ───────────────────│                          │
      │                            │                          │
      ❌ 事务回滚,数据一致

6.4 TCC 的三大经典问题

  TCC 虽然灵活,但引入了三个必须处理的经典问题:

① 空回滚(Empty Rollback):

  场景:Try 超时,TM 发起 Cancel,但 Try 的请求还没到达 RM
  → RM 收到 Cancel,但没有对应的 Try 记录
  → 解决:Cancel 阶段发现没有 Try 记录时,直接返回成功(允许空回滚)

  时序图:
    TM ── Try ──→ 网络延迟... ──→ RM(还没收到)
    TM 超时!
    TM ── Cancel ──→ RM(先到了!)
    RM:没有 Try 记录 → 允许空回滚 → 返回成功
    Try 请求终于到达 RM → RM 检查发现有 Cancel 标记 → 拒绝执行


② 幂等(Idempotent):

  场景:Try/Confirm/Cancel 可能被多次调用(网络重试、TM 重试)
  → 解决:使用 businessId 做唯一键,已处理则直接返回成功

  示例:
    Try(businessId="order123") → 第一次执行成功
    Try(businessId="order123") → 重试,发现已处理 → 直接返回成功


③ 悬挂(Suspension):

  场景:Cancel 先到达且执行完毕,之后 Try 才到达
  → Try 执行了但 Cancel 已经做过了,资源被错误冻结
  → 解决:Cancel 执行后标记 businessId 为已取消,Try 时检查标记,拒绝执行

  时序图:
    TM ── Cancel ──→ RM(先到)→ 执行 Cancel,标记已取消
    TM ── Try ──→ RM(后到)→ 检查标记 → 已被取消 → 拒绝执行

6.5 TCC 优缺点

✅ 优点:
  - 强一致性:Confirm 全部成功才算成功,任一失败就全部 Cancel
  - 性能优于 2PC:Try 阶段只锁定资源,不阻塞等待
  - 业务可控:开发者自定义 Try/Confirm/Cancel 逻辑,灵活性高

❌ 缺点:
  - 代码侵入大:每个业务都要写 Try/Confirm/Cancel 三个方法
  - 开发成本高:空回滚、幂等、悬挂都要处理
  - 不适合长流程:Confirm 阶段失败需要人工介入

七、Saga 模式 —— 长事务的最终一致性方案

7.1 核心思想

  Saga 的核心思想:把一个大事务拆成多个有序的小事务,每个小事务都有对应的补偿操作。如果某个步骤失败,依次执行之前步骤的补偿操作来"撤销"。

Saga 模式:

  正向:T1 → T2 → T3 → ... → Tn
  补偿:C1 ← C2 ← C3 ← ... ← Cn

  如果 T3 失败,依次执行 C2 → C1 回滚

  关键理解:
    - T1 提交后,T2 失败 → T1 已经提交了,不能回滚,只能"补偿"
    - 补偿不是数据库回滚,而是执行一段反向业务逻辑
    - 例如:退款补手续费、退货补运费

7.2 两种协调方式

① 编排式(Choreography,事件驱动):

  每个服务做完自己的事,发布事件,下一个服务监听事件

  订单服务 → 创建订单 → 发布"订单已创建"事件
  库存服务 → 监听事件 → 扣库存 → 发布"库存已扣减"事件
  账户服务 → 监听事件 → 扣余额 → 结束

  优点:松耦合,服务间无直接依赖
  缺点:流程分散在各服务中,难以追踪整体状态


② 编排式(Orchestration,命令驱动):

  一个 Saga 编排器统一调度所有步骤

  Saga 编排器:
    ① 调用订单服务创建订单
    ② 调用库存服务扣库存
    ③ 调用账户服务扣余额
    ④ 失败时按相反顺序调用补偿操作

  优点:流程集中管理,清晰可追踪
  缺点:编排器成为新的耦合点

7.3 Saga 优缺点

✅ 优点:
  - 高性能:每个小事务独立提交,不长时间锁定资源
  - 长流程友好:适合多步骤、跨服务的长事务
  - 松耦合:服务间通过事件或编排器通信

❌ 缺点:
  - 最终一致性:不保证实时一致,中间状态可见
  - 补偿复杂:补偿操作需要业务逻辑配合(如退款可能有手续费)
  - 隔离性弱:事务 A 扣了库存但还没提交,事务 B 可能读到中间状态

八、Seata AT 模式 —— 零侵入的自动挡方案

8.1 核心思想

  AT 模式的核心思想:自动拦截 SQL,记录修改前后的数据快照(前后镜像),回滚时自动生成反向 SQL。

AT 模式 = 两阶段提交 + 自动生成回滚 SQL

  阶段一(执行 + 记录快照):
    1. 解析 SQL,获取前镜像(before image)
    2. 执行业务 SQL
    3. 获取后镜像(after image)
    4. 将前后镜像 + 行锁信息插入 undo_log 表
    5. 提交本地事务(释放数据库连接)← 关键!

  阶段二-提交(清理):
    1. 异步删除 undo_log 记录

  阶段二-回滚(自动恢复):
    1. 从 undo_log 读取前镜像
    2. 校验后镜像是否匹配(防止脏写)
    3. 用前镜像生成反向 SQL 并执行
    4. 删除 undo_log 记录

8.2 AT 模式的全局锁

AT 模式的隔离性保障——全局锁:

  事务 A:UPDATE product SET stock = stock - 10 WHERE id = 1001
    → Seata 自动获取全局锁:lock:product:1001

  事务 B:UPDATE product SET stock = stock - 5 WHERE id = 1001
    → 发现全局锁被占用 → 等待或超时重试

  事务 A 提交 → 释放全局锁 → 事务 B 获取锁 → 执行

  这是 AT 模式防止脏写的关键机制

8.3 AT 模式优缺点

✅ 优点:
  - 零侵入:业务代码不需要任何修改,和单机事务一样写
  - 自动回滚:Seata 自动生成反向 SQL,不需要手写补偿
  - 性能较好:阶段一就提交本地事务,不长期占用连接

❌ 缺点:
  - 依赖数据库:undo_log 表是必须的,不支持非关系型数据库
  - 全局锁依赖 Seata Server:有单点风险(需要 Seata Server 高可用)
  - 隔离性:默认读未提交,可能读到脏数据
  - 仅支持 ACID 数据库:MySQL、PostgreSQL、Oracle

九、可靠消息最终一致性 —— 异步场景的最优解

9.1 核心思想

  可靠消息的核心思想:利用消息队列的可靠性,保证本地事务和消息发送的原子性。通过消息驱动下游服务完成数据同步。

可靠消息 = 本地事务 + 消息表 + 消息队列 + 重试机制

  ① 本地事务:执行业务操作 + 往"消息表"插入一条消息(同一个事务)
  ② 定时任务:扫描消息表,将"待发送"的消息发送到 MQ
  ③ 消费者:消费消息,执行业务逻辑
  ④ 确认删除:消费者执行成功后,消息表标记为"已发送"

9.2 两种实现方式

方式一:本地消息表

架构:
  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
  │  业务服务    │     │  消息表      │     │  消息队列    │
  │             │     │  (同库)      │     │  (RocketMQ) │
  │ ① 执行业务  │────→│ ② 写入消息   │     │             │
  │             │     │             │     │             │
  │             │     │ ③ 定时扫描  │────→│ ④ 发送消息  │
  └─────────────┘     └─────────────┘     └──────┬──────┘


                                           ┌─────────────┐
                                           │  下游服务    │
                                           │  ⑤ 消费消息  │
                                           └─────────────┘

  优点:通用方案,任何 MQ 都适用
  缺点:需要额外维护消息表,定时任务扫描有延迟

方式二:RocketMQ 事务消息

RocketMQ 事务消息 = 普通消息 + 事务状态回查

  ① 发送半消息(Half Message)→ MQ 保存但消费者不可见
  ② 执行本地事务
  ③ 本地事务成功 → 提交半消息 → MQ 变为可见,消费者可以消费
     本地事务失败 → 回滚半消息 → MQ 删除
  ④ 如果 MQ 没收到确认(超时),MQ 回查本地事务状态

  优点:不需要消息表,更优雅
  缺点:绑定 RocketMQ

9.3 可靠消息 vs TCC vs Saga

维度可靠消息TCCSaga
一致性最终一致强一致最终一致
回滚方式补偿(反向消息)Cancel补偿
实时性秒级实时秒级
耦合度低(异步解耦)高(同步调用)
适用场景通知、异步处理资金交易长流程业务

十、最大努力通知 —— 最简单的方案

10.1 核心思想

  最大努力通知:通知方尽最大努力通知,不保证一定成功。超过重试次数后放弃,记录异常,人工介入。

最大努力通知 = 简单重试 + 人工兜底

  ① 业务操作完成后,通知外部系统
  ② 如果通知失败,按一定策略重试(递增间隔)
  ③ 重试 N 次后仍失败,记录异常,人工介入

  典型场景:支付回调通知
    - 支付宝支付成功后,通知商户系统
    - 如果商户系统挂了,支付宝会重试多次
    - 超过最大重试次数,支付宝停止通知,商户可通过主动查询对账

10.2 与可靠消息的区别

可靠消息:
  有消息表或事务消息机制 → 保证消息一定发送成功
  适合:内部系统间数据同步

最大努力通知:
  没有消息表,只有重试 → 不保证一定成功
  适合:外部系统回调,不能控制对方系统

十一、六大方案全面对比

维度2PC(XA)TCCSagaAT(Seata)可靠消息最大努力通知
一致性强一致强一致最终一致最终一致最终一致最终一致
性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
复杂度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
代码侵入
回滚方式自动手动(Cancel)补偿自动(undo_log)补偿重试
锁资源时间
适用场景跨库事务资金交易长流程快速接入异步解耦外部回调
是否依赖数据库是(MySQL)

十二、方案选型指南

12.1 选型决策树

你的业务场景                              → 推荐方案
─────────────────────────────────────────────────────────────
强一致性、资金交易(转账、支付)            → TCC
强一致性、简单跨库事务(单体拆微服务)       → 2PC(XA)
长流程、非资金类(订单创建→审核→发货)      → Saga
无侵入、快速接入、非强一致                 → AT(Seata)
异步解耦、通知类(短信、邮件)              → 可靠消息
外部系统回调(支付回调、物流通知)           → 最大努力通知

12.2 实战建议

① 不要一上来就搞分布式事务
  先考虑业务上能否避免分布式事务:
    - 能不能把数据放在同一个数据库?
    - 能不能通过设计避免跨服务事务?
    - 能不能接受最终一致性?

② 能不用分布式事务就不用
  分布式事务 = 复杂度 × 10
  如果业务能接受最终一致性,优先用可靠消息

③ 选择你熟悉的技术栈
  Seata + AT 模式:最省事,零侵入
  Seata + TCC 模式:资金类业务首选
  RocketMQ 事务消息:异步场景首选

④ 必须做好监控
  分布式事务的异常情况远比单机事务多
  必须监控:成功率、耗时、重试次数、异常告警

十三、面试要点

Q1:什么是分布式事务?为什么需要它?

  分布式事务是指一次业务操作跨越多个服务、多个数据库时,需要保证数据一致性的技术方案。单机事务的 @Transactional 只能管理一个数据库,微服务架构下数据分散在多个数据库中,需要分布式事务来协调。

Q2:CAP 理论和 BASE 理论的关系?

  CAP 定理指出分布式系统最多只能同时满足一致性、可用性、分区容错性中的两项,P 必须满足,因此只能在 C 和 A 之间取舍。BASE 理论是对 AP 方案的补充,提出基本可用、软状态、最终一致性,指导我们在放弃强一致性后如何保证数据最终一致。

Q3:分布式事务有哪些实现方案?各有什么优缺点?

  六大方案:

  • 2PC(XA):强一致性,但性能差、有单点故障风险;
  • TCC:强一致性,但代码侵入大、开发成本高;
  • Saga:最终一致性,适合长流程,但补偿逻辑复杂;
  • AT(Seata):最终一致性,零侵入,但依赖数据库;
  • 可靠消息:最终一致性,异步解耦,适合通知类场景;
  • 最大努力通知:最终一致性,简单但有失败风险。

Q4:TCC 的空回滚、幂等、悬挂是什么?如何解决?

  空回滚:Cancel 时 Try 还没执行,解决方式是 Cancel 没找到 Try 记录时直接返回成功。幂等:同一操作重复执行,解决方式是用 businessId 做唯一键去重。悬挂:Cancel 先到 Try 后到,解决方式是 Cancel 后标记已取消,Try 时检查标记拒绝执行。

Q5:TCC 和 AT 模式有什么区别?

  TCC 需要手写 Try/Confirm/Cancel 三个方法,代码侵入大但灵活性高,适合资金类;AT 模式完全零侵入,Seata 自动生成反向 SQL,但依赖数据库的 undo_log 表,隔离性较弱。

Q6:什么场景不适合用分布式事务?

  1) 非核心业务,允许少量不一致(如统计、日志);2) 超高并发场景(秒杀),分布式事务会成为瓶颈;3) 跨公司/跨系统,无法控制对方数据库。这些场景用异步 + 对账 + 人工兜底更实际。


实战实现详见: 分布式事务与实现 —— 包含 Seata XA/TCC/Saga/AT 四种模式的完整代码示例、RocketMQ 事务消息实现、可靠消息本地消息表实现、最大努力通知实现,以及生产级最佳实践(幂等设计、监控告警、定时补偿等)。