4. 分布式事务:两阶段提交(2PC)、三阶段提交(3PC)、TCC模式
分布式事务,说白了就是让多个数据库或服务,要么一起成功,要么一起失败。听起来简单,做起来坑不少。我早年做金融批处理时,就因为这个栽过跟头——一个跨库转账,钱扣了但没加上,对账时才发现。嗯,从那以后,我对分布式事务就格外上心。
这一章,咱们聊聊三种主流方案:2PC、3PC 和 TCC。它们各有各的脾气,选对了是利器,选错了是噩梦。
4.1 两阶段提交(2PC)
2PC 是最经典的分布式事务协议。它有个协调者,负责指挥所有参与者。整个过程分两步:
- 准备阶段:协调者问每个参与者:“能提交吗?”参与者写日志、加锁,然后回复“可以”或“不行”。
- 提交阶段:如果所有人都说“可以”,协调者就广播“提交”;否则广播“回滚”。
核心思想:先问一圈,再统一行动。用锁和日志保证要么全做,要么全不做。
我画了个流程图,帮你理解这个流程:
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。别一上来就搞复杂的,容易翻车。