4. 分布式事务:两阶段提交(2PC)、三阶段提交(3PC)、TCC模式

分布式事务,说白了就是让多个数据库或服务,要么一起成功,要么一起失败。听起来简单,做起来坑不少。我早年做金融批处理时,就因为这个栽过跟头——一个跨库转账,钱扣了但没加上,对账时才发现。嗯,从那以后,我对分布式事务就格外上心。

这一章,咱们聊聊三种主流方案:2PC、3PC 和 TCC。它们各有各的脾气,选对了是利器,选错了是噩梦。

4.1 两阶段提交(2PC)

2PC 是最经典的分布式事务协议。它有个协调者,负责指挥所有参与者。整个过程分两步:

  1. 准备阶段:协调者问每个参与者:“能提交吗?”参与者写日志、加锁,然后回复“可以”或“不行”。
  2. 提交阶段:如果所有人都说“可以”,协调者就广播“提交”;否则广播“回滚”。

核心思想:先问一圈,再统一行动。用锁和日志保证要么全做,要么全不做。

我画了个流程图,帮你理解这个流程:

两阶段提交(2PC)流程图 协调者 参与者1 参与者2 参与者3 准备请求 准备完成 准备请求 准备完成 准备请求 准备完成 提交/回滚 全局提交 确认 准备阶段:询问 → 提交阶段:执行。任何参与者拒绝则全局回滚。

2PC 的优点很明显:实现简单,强一致性。但缺点也致命——同步阻塞。准备阶段,参与者要一直持有锁,直到协调者发来最终指令。如果协调者挂了,参与者就卡在那,资源释放不了。

避坑指南:我曾经在一个高并发支付系统里用 2PC,结果协调者宕机,所有参与者锁了 30 秒,业务直接熔断。后来我们加了超时和补偿机制,才勉强能用。

另外,2PC 是原子性的,但一致性并不完美。如果协调者在提交阶段挂了,部分参与者收到了提交,部分没收到,数据就不一致了。

4.2 三阶段提交(3PC)

3PC 是 2PC 的改良版。它把准备阶段拆成了两步:CanCommit 和 PreCommit,再加一个超时机制。你想想看,这样能解决什么问题?

  • CanCommit:协调者问大家:“能不能提交?”参与者只回答“能”或“不能”,不写日志、不加锁。
  • PreCommit:如果所有人都说“能”,协调者就发预提交。参与者写日志、加锁,然后回复“准备好了”。
  • DoCommit:协调者收到所有“准备好了”,就广播正式提交。

3PC 最大的改进是非阻塞。如果协调者在 PreCommit 后挂了,参与者会超时自动提交。为什么?因为能走到 PreCommit,说明所有人都同意过,所以默认提交是安全的。

个人经验:我建议在跨机房场景下优先考虑 3PC。网络分区时,2PC 容易死锁,3PC 的超时机制能自动恢复。但要注意,3PC 的协议更复杂,实现成本高。

不过,3PC 也有坑。如果网络出现分区,一部分参与者超时提交了,另一部分没收到提交指令,数据还是不一致。说白了,3PC 只是降低了阻塞概率,没完全解决。

特性 2PC 3PC
阶段数 2 3
阻塞性 同步阻塞 非阻塞(超时自动提交)
一致性 强一致性 最终一致性(有风险)
实现复杂度 低 中
适用场景 小规模、低并发 跨机房、网络不稳定

4.3 TCC 模式

TCC 是另一种思路。它不依赖锁和日志,而是通过业务逻辑来保证一致性。TCC 代表三个操作:

  • Try:预留资源。比如扣库存时,先冻结一部分,不真扣。
  • Confirm:确认执行。如果所有 Try 都成功,就执行 Confirm,真正扣库存。
  • Cancel:取消。如果任何一个 Try 失败,就执行 Cancel,释放预留资源。

TCC 的好处是高性能,没有锁,适合高并发。但坏处是业务侵入性强——你得为每个操作写 Try、Confirm、Cancel 三个方法。

核心思想:把分布式事务拆成业务补偿操作。Try 是准备,Confirm 是提交,Cancel 是回滚。

举个例子,一个下单流程:

// Try:冻结库存
boolean tryResult = inventoryService.freezeStock(orderId, productId, quantity);
if (!tryResult) {
    // 冻结失败,执行 Cancel
    cancelAll();
    return;
}

// Try:冻结余额
tryResult = accountService.freezeBalance(orderId, userId, totalPrice);
if (!tryResult) {
    // 冻结失败,执行 Cancel
    cancelAll();
    return;
}

// 所有 Try 成功,执行 Confirm
confirmAll();

void confirmAll() {
    inventoryService.deductStock(orderId, productId, quantity);
    accountService.deductBalance(orderId, userId, totalPrice);
}

void cancelAll() {
    inventoryService.unfreezeStock(orderId, productId, quantity);
    accountService.unfreezeBalance(orderId, userId, totalPrice);
}

TCC 的难点在于幂等性。Confirm 和 Cancel 可能被重复调用,你得保证它们执行多次和一次效果一样。我见过一个项目,Cancel 没做幂等,结果重复退款,亏了不少钱。

避坑指南:我曾经在一个支付系统里用 TCC,Try 阶段预留了余额,但 Confirm 时网络超时,框架重试了三次。幸好我们做了幂等,不然用户会被扣三次钱。记住:TCC 的每个操作都要幂等。

另外,TCC 的空回滚问题也要注意。如果 Try 没执行,但 Cancel 被调用了,你得能识别出来,不能报错。通常用事务 ID 或状态机来判断。

4.4 如何选择?

这三种模式没有银弹。我一般这样选:

  • 2PC:适合小规模、低并发、强一致性要求高的场景。比如银行转账,数据不能错,慢点可以接受。
  • 3PC:适合跨机房、网络不稳定的场景。比如全球部署的系统,网络分区是常态。
  • TCC:适合高并发、业务逻辑清晰的场景。比如电商秒杀,性能优先,业务补偿可控。

你想想看,如果你的系统一天只有几百笔交易,用 2PC 完全没问题。但如果每秒几千笔,TCC 才是正解。别为了追求“先进”而选 3PC,它带来的复杂度可能得不偿失。

个人建议:如果团队经验不足,先从 2PC 开始。等踩过坑、理解了分布式事务的本质,再考虑 TCC 或 3PC。别一上来就搞复杂的,容易翻车。