Appearance
分布式事务
分布式事务是微服务架构中最核心的难题之一。当一次业务操作跨越多个服务进程,触发多个彼此隔离的本地事务时,如何保证数据在全局范围内的原子性和一致性?
需要特别澄清的是,这里的“分布式”并不等同于“分库分表”。即使多个微服务共享同一个物理数据库,只要每个服务通过独立的数据库连接(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 + 账户 B | A 扣钱 + 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、TCC | Saga、可靠消息、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 回查本地事务状态
优点:不需要消息表,更优雅
缺点:绑定 RocketMQ9.3 可靠消息 vs TCC vs Saga
| 维度 | 可靠消息 | TCC | Saga |
|---|---|---|---|
| 一致性 | 最终一致 | 强一致 | 最终一致 |
| 回滚方式 | 补偿(反向消息) | Cancel | 补偿 |
| 实时性 | 秒级 | 实时 | 秒级 |
| 耦合度 | 低(异步解耦) | 高(同步调用) | 中 |
| 适用场景 | 通知、异步处理 | 资金交易 | 长流程业务 |
十、最大努力通知 —— 最简单的方案
10.1 核心思想
最大努力通知:通知方尽最大努力通知,不保证一定成功。超过重试次数后放弃,记录异常,人工介入。
最大努力通知 = 简单重试 + 人工兜底
① 业务操作完成后,通知外部系统
② 如果通知失败,按一定策略重试(递增间隔)
③ 重试 N 次后仍失败,记录异常,人工介入
典型场景:支付回调通知
- 支付宝支付成功后,通知商户系统
- 如果商户系统挂了,支付宝会重试多次
- 超过最大重试次数,支付宝停止通知,商户可通过主动查询对账10.2 与可靠消息的区别
可靠消息:
有消息表或事务消息机制 → 保证消息一定发送成功
适合:内部系统间数据同步
最大努力通知:
没有消息表,只有重试 → 不保证一定成功
适合:外部系统回调,不能控制对方系统十一、六大方案全面对比
| 维度 | 2PC(XA) | TCC | Saga | AT(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 事务消息实现、可靠消息本地消息表实现、最大努力通知实现,以及生产级最佳实践(幂等设计、监控告警、定时补偿等)。
