分布式事务核心:从2PC到TCC的实战之路
大家好,我是你们的老朋友。今天我们来聊聊分布式事务这块硬骨头。
说实话,分布式事务是很多团队绕不过去的坎。我见过不少项目,前期图省事不用分布式事务,结果数据不一致了,半夜爬起来修数据,那叫一个酸爽。所以,咱们今天就把2PC、3PC、TCC这三个核心方案彻底讲透。
一、2PC(两阶段提交):最经典的分布式事务协议
2PC,全称Two-Phase Commit。说白了,就是一个协调者带着一群参与者,分两步走完一个事务。
1.1 第一阶段:投票阶段
协调者问所有参与者:“我要提交了,你们准备好了吗?”
- 参与者各自执行本地事务,但先不提交
- 写undo/redo日志,保证能回滚
- 返回Yes或No给协调者
1.2 第二阶段:提交/回滚阶段
协调者根据投票结果做决策:
- 如果所有人都说Yes,那就广播Commit
- 只要有一个人说No,那就广播Rollback
核心要点:2PC是强一致性协议,要么全部成功,要么全部失败。但它的阻塞问题很要命——参与者一直持有资源锁,直到收到协调者的最终指令。
我的经验:我在做金融交易系统时用过2PC。当时有个场景,协调者挂了,参与者一直等Commit指令,锁了半小时。从那以后,我建议所有用2PC的团队,必须给协调者做高可用,否则就是给自己埋雷。
1.3 2PC的代码骨架
// 协调者伪代码
public class Coordinator {
public boolean executeTransaction() {
// 第一阶段:投票
for (Participant p : participants) {
boolean ready = p.prepare();
if (!ready) {
// 有人投No,回滚
for (Participant p2 : participants) {
p2.rollback();
}
return false;
}
}
// 第二阶段:提交
for (Participant p : participants) {
p.commit();
}
return true;
}
}
注意:2PC在第二阶段如果网络分区,参与者可能永远收不到Commit指令。这就是所谓的“悬挂事务”。我曾经遇到过,最后只能人工介入,手动查日志确认状态。
二、3PC(三阶段提交):2PC的改进版
3PC把2PC的两阶段拆成了三阶段,核心思想是引入超时机制,减少阻塞时间。
2.1 三个阶段详解
| 阶段 | 动作 | 说明 |
|---|---|---|
| CanCommit | 询问能否提交 | 只是问问,不执行任何操作 |
| PreCommit | 预提交 | 执行事务,写日志,但不提交 |
| DoCommit | 最终提交 | 提交或回滚 |
你想想看,3PC比2PC多了一个CanCommit阶段。这个阶段的好处是:如果某个参与者明显不行,那就不用浪费资源去执行PreCommit了。
关键改进:3PC引入了超时机制。参与者在PreCommit阶段如果超时没收到DoCommit指令,会自动提交。这解决了2PC的阻塞问题,但也带来了数据不一致的风险——万一协调者想回滚,但参与者已经自动提交了呢?
2.2 3PC的优缺点
- 优点:减少了阻塞时间,参与者可以超时自决
- 缺点:引入了新的不一致场景,实现复杂度更高
我的建议:3PC在实际项目中用得不多。我个人更倾向于用TCC或者Saga。3PC的理论价值大于实践价值,但理解它能帮你更好地理解分布式事务的演进思路。
三、TCC(Try-Confirm-Cancel):实战之王
TCC是现在最流行的分布式事务方案。它把每个服务的事务操作拆成三步:Try、Confirm、Cancel。
3.1 TCC的三个阶段
- Try:预留资源。比如扣库存时,先冻结库存,不是直接扣减
- Confirm:确认执行。把预留的资源真正扣掉
- Cancel:取消操作。释放预留的资源
核心思想:TCC是业务层面的分布式事务,不是数据库层面的。它要求每个服务自己实现Try、Confirm、Cancel三个接口。说白了,就是把分布式事务的控制权交给业务代码。
3.2 TCC实战代码示例
// 库存服务的TCC实现
public class InventoryService {
// Try阶段:冻结库存
public boolean tryDeduct(Long orderId, Long productId, int quantity) {
// 检查库存是否充足
int available = getAvailableStock(productId);
if (available < quantity) {
return false; // 库存不足,Try失败
}
// 冻结库存
freezeStock(productId, quantity);
// 记录Try日志,用于后续Confirm或Cancel
saveTryLog(orderId, productId, quantity);
return true;
}
// Confirm阶段:真正扣减库存
public boolean confirmDeduct(Long orderId) {
TryLog log = getTryLog(orderId);
if (log == null) return false;
// 从冻结库存中扣减
deductFrozenStock(log.productId, log.quantity);
// 删除Try日志
deleteTryLog(orderId);
return true;
}
// Cancel阶段:释放冻结库存
public boolean cancelDeduct(Long orderId) {
TryLog log = getTryLog(orderId);
if (log == null) return false;
// 释放冻结库存
unfreezeStock(log.productId, log.quantity);
// 删除Try日志
deleteTryLog(orderId);
return true;
}
}
避坑指南:我曾经在TCC的Cancel阶段踩过坑。当时Cancel接口幂等没做好,重复调用了两次,结果库存被释放了两次,变成了负数。记住:TCC的所有接口都必须幂等!
3.3 TCC的适用场景
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 短事务,高并发 | TCC | 资源锁持有时间短,性能好 |
| 长事务,低并发 | 2PC | 强一致性,但锁时间长 |
| 最终一致性可接受 | Saga | 无锁,但需要补偿机制 |
四、三种方案对比总结
嗯,到这里,三种方案的核心内容都讲完了。我画了一张图,帮你快速理清思路:
最后说一句:分布式事务没有银弹。我见过太多团队盲目追求“完美方案”,结果把系统搞得很复杂。我的建议是:能不用分布式事务就不用,实在要用,优先考虑TCC。如果业务允许最终一致性,Saga也是好选择。
好了,这一章的内容就到这里。记住:理解原理比背代码更重要。下次遇到分布式事务问题,先想清楚你的业务场景,再选方案。
可可精品资料库