分布式事务核心理论
本地事务靠数据库的 ACID 保证,一旦业务拆分到多个服务、多个数据库,跨节点的事务就成了最难的环节。本文从理论出发,把 CAP、BASE、2PC、3PC、TCC、SAGA、消息最终一致性这些概念讲透,并给出场景选型的方法。
从本地事务到分布式事务
本地事务的边界
本地事务(单库单机):
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;
ACID 全部由数据库保证:原子性、一致性、隔离性、持久性微服务化后,一次业务往往要写多个库、调多个服务:
下单 → 订单库(扣库存)→ 账户库(扣余额)→ 积分库(加积分)每个库各自开启事务,彼此的提交/回滚互不知情,就出现了分布式事务问题。
问题本质
分布式事务的本质是:多个节点上的多个操作,如何让它们"要么全部成功、要么全部失败",且对外表现一致。因为不存在一个"超级数据库"来统一管理所有节点,所以需要一套协调机制,这就引出了 CAP 理论。
CAP 理论
三要素
| 要素 | 含义 | 说明 |
|---|---|---|
| Consistency(一致性) | 所有节点同一时刻看到相同数据 | 写入后任何读取都能读到最新值 |
| Availability(可用性) | 每个请求都能在合理时间内获得响应 | 不保证响应中的数据是最新的 |
| Partition Tolerance(分区容错性) | 网络分区(节点间通信失败)时系统仍能运行 | 分布式系统必须满足 |
核心结论
网络分区在分布式环境下是必然发生的,所以 P 必须满足,只能在 C 与 A 之间二选一:
网络分区发生时:
┌─ 选择 CP:宁可暂时拒绝服务,也要保证数据一致
│ 例子:Zookeeper、Nacos(CP 模式)、etcd、HBase
│
└─ 选择 AP:宁可数据暂时不一致,也要保证服务可用
例子:Eureka、Nacos(AP 模式)、Cassandra、DynamoDB与分布式事务的关系
CAP 对分布式事务的指导:
├─ 追求强一致 → 2PC / 3PC / XA(CP 思路,牺牲部分可用性)
├─ 追求最终一致 → TCC / SAGA / 消息方案(AP 思路,用补偿换取可用性)
└─ 没有银弹:一致性越强,可用性与性能越差BASE 理论
BASE 是 CAP 中 AP 思路的延伸,强调"基本可用 + 最终一致":
| 缩写 | 全称 | 含义 |
|---|---|---|
| BA | Basically Available | 基本可用:核心功能可用,允许部分降级 |
| S | Soft State | 软状态:允许中间状态存在(如"处理中") |
| E | Eventually Consistent | 最终一致:经过一段时间后数据收敛一致 |
BASE 的思路:
不追求任何时刻都一致,而是保证系统可用,
用异步补偿、重试、对账等手段,让数据最终一致。本地消息表、事务消息、TCC、SAGA 都是 BASE 思想的落地。
一致性模型
| 模型 | 特点 | 适用场景 |
|---|---|---|
| 强一致 | 提交后立即可见,无中间状态 | 转账、余额扣减、库存扣减 |
| 弱一致 | 提交后不一定立刻可见 | 缓存、排行榜 |
| 最终一致 | 一段时间后收敛一致 | 积分、通知、订单状态流转 |
选型原则:核心资金链路用强一致,非核心链路用最终一致,不要一刀切。
两阶段提交(2PC)
协议流程
2PC 引入一个协调者(Coordinator),把事务拆成两个阶段:
阶段一:Prepare(投票)
协调者 → 所有参与者:预执行事务,能提交就返回 YES,否则返回 NO
协调者收集所有参与者的投票
阶段二:Commit / Abort(提交或中止)
全部 YES → 协调者通知所有参与者 COMMIT
任一 NO → 协调者通知所有参与者 ROLLBACK协调者 参与者
│ │
├── Prepare ────────────▶│ 执行本地事务(不提交)
│◀──── YES/NO ───────────┤
│ │
├── 收集投票 │
│ │
├── 全 YES → Commit ────▶│ 提交本地事务
│ 任一 NO → Rollback ──▶│ 回滚本地事务实现形式
| 形式 | 说明 |
|---|---|
| 应用层实现 | 业务代码自己实现协调者与参与者接口 |
| XA 协议 | 数据库层面支持,XA START / XA END / XA PREPARE / XA COMMIT |
| 框架封装 | Seata XA 模式、Atomikos、Narayana 等 |
存在的问题
2PC 三大痛点:
├─ 同步阻塞:Prepare 后参与者持有资源锁,等待协调者指令,期间阻塞其他事务
├─ 单点故障:协调者挂了,所有参与者卡在"待命"状态无法决策
└─ 脑裂风险:阶段二协调者与部分参与者失联,提交指令丢失,数据不一致三阶段提交(3PC)
3PC 在 2PC 基础上多了一个阶段,并引入超时机制,降低阻塞:
阶段一:CanCommit(询问)
协调者 → 参与者:能否提交?(只问状态,不执行)
参与者 → 协调者:YES / NO
阶段二:PreCommit(预提交)
都 YES → 协调者通知预提交,参与者执行事务但不提交
有 NO → 协调者通知中止
阶段三:DoCommit(最终提交)
参与者收到指令后提交;若超时未收到指令,按预提交状态自行提交与 2PC 的对比
| 对比项 | 2PC | 3PC |
|---|---|---|
| 阶段数 | 2 | 3 |
| 超时机制 | 无(参与者傻等) | 有(参与者超时可自行决策) |
| 阻塞程度 | 高 | 较低 |
| 实现复杂度 | 低 | 高 |
| 实际使用 | XA、Seata XA | 生产环境极少,理论价值为主 |
TCC 模式
TCC(Try-Confirm-Cancel)把业务操作拆成三个阶段,用补偿换取不锁资源:
Try :预留资源(如冻结余额 100 元,不入账)
Confirm :确认执行(冻结的 100 元真正扣减)
Cancel :取消预留(释放冻结的 100 元)下单流程(TCC 视角):
Try:账户服务冻结 100 元;库存服务预占 1 件;订单服务创建"待确认"订单
└─ 全部 Try 成功 → Confirm 阶段
└─ 任一 Try 失败 → Cancel 阶段,各服务释放预留资源
Confirm:冻结转扣减、预占转扣减、订单置为已创建
Cancel :冻结释放、预占释放、订单置为已取消优缺点
| 优点 | 缺点 |
|---|---|
| 不锁数据库资源,性能好 | 业务侵入大,每个接口要写三套逻辑 |
| 可应对长事务 | Try 阶段要保证幂等,Confirm/Cancel 要可重入 |
| 适用金额等敏感场景 | 对并发控制(防悬挂、防重复提交)要求高 |
关键问题
TCC 的三个坑:
├─ 空回滚:Try 没执行成功,Cancel 却被调用了(要能识别"预留不存在")
├─ 悬挂:Cancel 先于 Try 到达(要拒绝"没有 Try 过的 Cancel")
└─ 幂等:Confirm / Cancel 可能被重复调用,必须幂等SAGA 模式
SAGA 把一个长事务拆成一串本地事务,每个本地事务配一个补偿事务:
SAGA 正向执行:
T1(扣库存)→ T2(扣余额)→ T3(加积分)
SAGA 失败回滚(反向补偿):
T3 失败 → 补偿 C2(退还余额)→ 补偿 C1(回补库存)两种编排方式
编排式(Choreography):
各服务通过消息互相触发,没有中心协调者
优点:去中心化;缺点:流程隐晦、难追踪、难调试
订单服务发"扣库存"事件 → 库存服务扣减后发"扣余额"事件 → 账户服务扣减...
任一环节失败 → 反向发补偿事件
编排式(Orchestration):
一个中心协调器(Saga Orchestrator)编排所有步骤
优点:流程清晰、易监控;缺点:协调器成为中心点
Orchestrator:调用 T1 → 调用 T2 → 调用 T3
失败 → 依次调用 C2、C1与 TCC 对比
| 对比项 | TCC | SAGA |
|---|---|---|
| 粒度 | 每个参与接口拆 Try/Confirm/Cancel | 每个本地事务一个补偿 |
| 资源预留 | Try 阶段预留 | 不预留,直接执行 |
| 一致性 | 较强 | 较弱(中间状态可见) |
| 实现成本 | 高 | 中 |
| 典型场景 | 资金、库存强管控 | 长流程业务(预订、订单) |
消息最终一致性
利用消息队列作为协调媒介,业务与消息"同库同事务"或"事务消息"保证最终一致:
方案一:本地消息表
业务表 + 消息表在同一数据库、同一本地事务
写业务 + 写消息表 → 一起提交
定时任务扫描消息表 → 投递 MQ → 收到确认后标记已发送
方案二:事务消息(RocketMQ)
发送半消息 → 执行本地事务 → 提交/回滚
本地事务结果不确定 → Broker 回查
方案三:MQ 事务 + 消费幂等
消费端必须幂等,因为消息可能重复投递消息方案的共识是:发送端尽力保证投递,接收端幂等消费,缺失靠对账补。
场景选型
选型决策树
业务需要强一致吗?
├─ 是(资金核心链路)
│ ├─ 数据库支持 XA → Seata XA / 2PC
│ └─ 业务可控、需高性能 → TCC
│
└─ 否(最终一致即可)
├─ 有现成消息队列 → 事务消息 / 本地消息表
├─ 长流程多步骤 → SAGA
└─ 简单跨库操作 → Seata AT各方案适用场景速查
| 方案 | 一致性 | 侵入性 | 性能 | 典型场景 |
|---|---|---|---|---|
| 2PC / XA | 强一致 | 低(数据库层) | 低(锁资源) | 银行转账、同构数据库 |
| 3PC | 强一致 | 高 | 中 | 理论为主 |
| TCC | 较强 | 高(三接口) | 高 | 资金、库存强管控 |
| SAGA | 最终一致 | 中 | 高 | 长流程订单、预订 |
| AT 模式 | 最终一致 | 低(自动生成 SQL) | 中 | 跨库业务快速接入 |
| 本地消息表 | 最终一致 | 中 | 中 | 与 MQ 结合的场景 |
| 事务消息 | 最终一致 | 低 | 高 | 下单发消息、异步通知 |
组合使用
生产上往往不是单一方案,而是组合:
下单全链路示例:
├─ 订单与库存跨库:Seata AT(或 TCC)保证强一致
├─ 订单与积分:事务消息(最终一致,允许异步)
└─ 订单与短信通知:本地消息表 + 定时重试(尽力而为)总结
分布式事务没有万能方案,核心是看清业务对一致性的真实要求:资金强一致走 2PC/XA/TCC,长流程走 SAGA,异步场景走消息最终一致,跨库简单场景用 AT 模式最省力。理解了 CAP/BASE 和各类协议的原理,面对任何场景都能选出合适的落地方案。