Skip to content

分布式事务

在微服务架构中,一个业务操作往往跨越多个服务和数据库,单个数据库的本地事务无法保证跨服务的数据一致性。

电商下单场景:
订单服务:创建订单(订单DB)
库存服务:扣减库存(库存DB)
账户服务:扣减余额(账户DB)
问题:如何保证三个操作要么全部成功,要么全部回滚?
→ 每个服务有自己的数据库,无法使用同一个 ACID 事务

2PC(Two-Phase Commit,两阶段提交)

Section titled “2PC(Two-Phase Commit,两阶段提交)”

2PC 是一种经典的分布式事务协议,适用于单个协调者协调多个资源管理器的场景。

参与者:
协调者(Coordinator):事务管理器,如 Java 中的 JTA
参与者(Participant):资源管理器,如数据库
第一阶段:准备(Prepare / Voting)
协调者 → 所有参与者:Prepare(准备提交)
每个参与者:
执行事务操作,写入 undo/redo 日志
但不提交(持有锁)
回复:Yes(可以提交)或 No(出现问题)
第二阶段:提交/回滚(Commit / Rollback)
所有参与者都回复 Yes → 协调者发送 Commit
任意一个参与者回复 No → 协调者发送 Rollback
参与者提交或回滚,释放锁,回复 ACK
1. 同步阻塞(Blocking):
Prepare 阶段参与者持有锁等待 Commit/Rollback
如果协调者崩溃,参与者永久阻塞
2. 单点故障(Single Point of Failure):
协调者崩溃导致所有参与者阻塞,系统无法继续
3. 脑裂(Split Brain):
协调者发送 Commit 后崩溃,部分参与者提交,部分未收到 → 数据不一致
4. 性能:
持锁时间 = 两个网络往返(至少 2 RTT),加上数据库操作时间
高并发场景下锁竞争严重

3PC 在 2PC 的基础上增加了一个”预提交”阶段,并引入超时机制,试图解决 2PC 的阻塞问题。

第一阶段:CanCommit
协调者询问参与者"能否提交?"
参与者只做检查(不锁资源),回复 Yes/No
第二阶段:PreCommit
所有 Yes → 协调者发 PreCommit
参与者执行事务,写日志,持有锁
参与者有超时:超时后自动提交(区别于 2PC 超时后阻塞)
第三阶段:DoCommit
协调者发 DoCommit → 参与者提交
改进:
参与者有超时自动提交(减少阻塞)
CanCommit 阶段不持有锁
3PC 仍未完全解决脑裂问题,且实现复杂,实际中很少直接使用

TCC 是一种应用层面的分布式事务方案,通过业务代码自己实现补偿逻辑。

三个阶段:
Try(预留):检查并预留资源(不直接操作,只冻结)
Confirm(确认):所有 Try 成功后执行实际业务操作
Cancel(取消):Try 失败或超时后释放预留资源
以转账为例:
Try:
账户A:检查余额 ≥ 100,冻结 100 元(balance -= 100, frozen += 100)
账户B:检查账户有效性,创建冻结记录
全部 Try 成功 → Confirm:
账户A:解除冻结,余额减少已生效(frozen -= 100)
账户B:增加余额,释放冻结记录(balance += 100)
任意 Try 失败 → Cancel:
账户A:解冻(frozen -= 100, balance += 100)
账户B:删除冻结记录
1. 幂等性(Idempotency):
网络重试导致 Confirm/Cancel 可能重复调用
每个操作必须幂等(相同操作执行多次结果不变)
方案:使用全局唯一 txId 记录已处理的请求,重复请求直接返回成功
2. 空回滚(Empty Rollback):
Try 由于网络问题未到达,但 Cancel 先到了
Cancel 必须能处理 Try 未执行的情况(直接返回成功)
方案:Try 时写记录,Cancel 时检查记录是否存在
3. 悬挂(Hanging / Suspend):
网络拥塞导致 Cancel 先执行完,Try 才到达
Try 执行后资源会被永久冻结(没有对应的 Cancel 了)
方案:Cancel 时记录"该 txId 已被回滚",Try 到来时检查,若已回滚则直接返回

TCC 适用场景:对一致性要求较高、需要强隔离性的场景,如金融交易。代价是需要为每个业务操作实现 Try/Confirm/Cancel 三个接口,开发成本较高。


Saga 将长事务分解为一系列有序的本地事务,每个本地事务有对应的补偿事务。

Saga 的两种协调模式:
1. 编排(Choreography):
没有中央协调者,服务间通过消息/事件驱动
订单创建 → 发布"订单创建"事件
库存服务消费事件 → 扣减库存 → 发布"库存扣减成功"事件
账户服务消费事件 → 扣减余额 → 发布"余额扣减成功"事件
优点:松耦合;缺点:事务流程分散,难以追踪
2. 编排(Orchestration):
中央 Saga 协调者按顺序调用各服务
SagaOrchestrator:
1. 调用 OrderService.createOrder() → 成功
2. 调用 InventoryService.deductStock() → 成功
3. 调用 AccountService.deductBalance() → 失败!
4. 补偿:调用 InventoryService.restoreStock()
5. 补偿:调用 OrderService.cancelOrder()
优点:流程清晰,便于监控;缺点:协调者可能成为单点
Saga vs TCC:
Saga 没有 Try 阶段(直接执行),隔离性较差(中间状态对外可见)
TCC 有 Try 预留阶段,隔离性更好,但实现更复杂
Saga 适合:允许中间状态暂时暴露的长流程业务(如电商订单流程)
TCC 适合:需要强隔离性的金融场景

本地消息表(可靠消息最终一致性)

Section titled “本地消息表(可靠消息最终一致性)”

这是生产环境中最常用的分布式事务方案,实现了最终一致性,成本低。

场景:订单创建后发送消息通知积分服务
本地消息表方案:
订单服务数据库:
orders 表(主业务)
local_messages 表(本地消息,与 orders 在同一数据库)
步骤 1:本地事务(原子)
BEGIN TRANSACTION
INSERT INTO orders(...) -- 创建订单
INSERT INTO local_messages(...) -- 写入消息记录(status=PENDING)
COMMIT
步骤 2:定时任务(或触发器)
查询 local_messages 中 status=PENDING 的记录
发送到消息队列
成功后更新 status=SENT
步骤 3:消费者(积分服务)
消费消息 → 幂等处理(防重复消费)→ 执行业务
步骤 4:消息确认
消费者 ACK → 消息队列删除消息
订单服务定时任务更新 local_messages status=DONE
特点:
✓ 实现简单,与业务代码侵入性小
✓ 生产可靠,消息不丢失(本地事务保证)
✗ 中间状态对外可见(最终一致性,不是强一致性)
✗ 需要处理消费者的幂等性

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的分布式事务框架,提供 AT、TCC、Saga、XA 四种模式。

AT 模式对业务代码无侵入,通过拦截 SQL 自动生成 undo log 实现回滚。

组件:
TC(Transaction Coordinator):Seata Server,管理全局事务状态
TM(Transaction Manager):事务发起方,声明全局事务边界
RM(Resource Manager):各微服务,管理分支事务
两阶段:
一阶段(执行业务):
1. TM 向 TC 注册全局事务,获取 XID
2. 每个 RM 执行本地 SQL,同时:
- 解析 SQL,查询 before image(数据执行前的快照)
- 执行业务 SQL
- 查询 after image(数据执行后的快照)
- 写入 undo_log(before image + after image)
- 向 TC 注册分支事务,提交本地事务(锁释放)
二阶段(提交/回滚):
全部成功 → TC 通知各 RM 删除 undo_log(已提交,无需回滚)
失败 → TC 通知各 RM 用 undo_log 中的 before image 进行数据回滚
特点:
✓ 对业务代码几乎无侵入(只需 @GlobalTransactional 注解)
✓ 性能好(一阶段就释放了数据库锁)
✗ 基于数据库的 undo log,依赖特定数据库
✗ 全局事务期间若有其他事务修改数据,可能造成脏写(需全局锁配合)

方案对比:
方案 一致性 性能 侵入性 适用场景
─────────────────────────────────────────────────────
2PC 强一致性 低 低 数据库 XA 事务
TCC 强一致性 中 高 金融高一致场景
Saga 最终一致性 高 中 长流程业务
本地消息表 最终一致性 高 低 跨服务通知,允许最终一致
Seata AT 最终一致性 中 极低 业务改造成本低,一般业务场景
实际建议:
大多数业务(电商、积分、通知)→ 本地消息表 + 消息队列(最终一致)
金融转账、扣款 → TCC(强隔离)
遗留系统改造,无法修改业务代码 → Seata AT
长流程审批(无法回滚只能补偿)→ Saga

Q:2PC 有什么问题?

主要有三个:①同步阻塞,Prepare 阶段参与者持有锁等待协调者,协调者崩溃导致所有参与者无限阻塞;②单点故障,协调者是单点,崩溃影响整个事务;③脑裂,协调者发出 Commit 后崩溃,部分参与者提交部分未提交,数据不一致。

Q:TCC 的 Cancel 操作需要注意什么?

需要处理三种特殊情况:①幂等性,Cancel 可能因重试被多次调用,必须保证幂等;②空回滚,Try 未到达但 Cancel 先到,Cancel 要能处理未 Try 的情况直接成功;③悬挂,Cancel 先执行完后 Try 才到达,需要记录”已回滚”状态,阻止后续的 Try 执行。

Q:如何保证消息队列的可靠传递(不丢不重)?

不丢:生产者使用本地消息表 + 定时重发,确认机制(confirm);消息队列持久化;消费者手动 ACK(处理完业务再确认)。不重:消费者实现幂等性,通过唯一消息 ID 或业务 ID 去重(Redis SET、数据库唯一索引)。

Q:Saga 和 TCC 有什么区别?

TCC 有 Try 预留阶段,在预留阶段冻结资源,提供较强的隔离性,中间状态不对外暴露;实现复杂,需要为每个操作写三个方法。Saga 没有预留阶段,直接执行本地事务,中间状态对外可见(最终一致);实现相对简单,适合允许临时不一致的长流程业务。