3、强一致性方案之3PC:三阶段提交协议详解
聊完2PC,咱们来看看它的升级版——3PC。
说实话,我第一次接触3PC时,心里想的是:「多一个阶段就能解决问题了?」后来在项目中踩过坑才明白,这个「多一阶段」的设计,其实挺巧妙的。
3.1 为什么需要3PC?
2PC有个硬伤:协调者单点故障。
我记得有一次线上压测,协调者节点突然挂了。所有参与者都卡在「准备」状态,资源锁着不放。业务方急得跳脚,我们只能手动清理事务。那场面,真叫一个狼狈。
2PC的另一个问题是:参与者太被动。协调者挂了,参与者只能傻等。没人告诉它们该提交还是回滚。
3PC就是来解决这两个问题的。它引入了超时机制,还加了一个「预提交」阶段。说白了,就是给参与者更多自主权。
3.2 三阶段提交的核心流程
3PC把事务提交拆成了三步:CanCommit、PreCommit、DoCommit。
你想想看,这就像项目评审会:先问大家「能不能做?」(CanCommit),再问「准备好了吗?」(PreCommit),最后说「开始干!」(DoCommit)。
第一阶段:CanCommit(询问阶段)
协调者问所有参与者:「这个事务你们能处理吗?」
参与者只需要检查自身状态:网络通不通?资源够不够?事务能不能执行?
注意,这里不执行任何实际操作,只是做个可行性检查。我见过不少新手把这一步和PreCommit搞混,结果在CanCommit里就锁了资源,导致系统卡死。
- 协调者发送CanCommit请求
- 参与者回复Yes/No
- 如果有任何一个No,协调者就中止事务
第二阶段:PreCommit(预提交阶段)
如果所有参与者都回复Yes,协调者就进入PreCommit。
这一步,参与者要真正执行事务操作,但先不提交。把数据写入undo/redo日志,资源锁住。
嗯,这里有个关键点:参与者开始有超时机制了。如果在一定时间内没收到协调者的DoCommit指令,参与者可以自行中止事务。
第三阶段:DoCommit(提交阶段)
协调者确认所有参与者都PreCommit成功后,发送DoCommit指令。
参与者收到后,正式提交事务,释放资源。
如果协调者挂了怎么办?参与者会等待超时。超时后,默认执行提交。为什么是提交而不是回滚?因为能走到DoCommit,说明所有参与者都准备好了,提交是更安全的选择。
3.3 3PC如何改进2PC?
咱们来对比一下,3PC到底改进了什么。
| 对比项 | 2PC | 3PC |
|---|---|---|
| 阶段数 | 2个 | 3个 |
| 参与者超时 | 无 | 有(PreCommit和DoCommit阶段) |
| 协调者故障处理 | 参与者阻塞等待 | 参与者超时后自主决策 |
| 资源锁定时间 | 较长(从Prepare到Commit) | 较短(从PreCommit到DoCommit) |
| 数据一致性 | 强一致 | 强一致(极端情况有风险) |
说白了,3PC最大的改进就是:让参与者不再傻等。
我曾经在一个金融项目中用2PC,协调者挂了,所有参与者锁了20分钟。换成3PC后,超时时间设为5秒,参与者自己就做决策了。虽然偶尔会有不一致的风险,但配合补偿机制,整体可用性提升了一大截。
3.4 3PC的优缺点
任何方案都有取舍,3PC也不例外。
优点
- 减少阻塞:参与者超时后能自主决策,不会无限等待
- 提高可用性:协调者故障时,系统仍能继续运行
- 资源释放更快:PreCommit阶段才锁资源,比2PC的Prepare阶段更晚
缺点
- 网络开销增加:多了一次通信,延迟更高
- 极端情况不一致:超时提交策略在特定场景下会导致数据不一致
- 实现复杂:状态机、超时处理、异常恢复,代码量比2PC多不少
3.5 3PC的状态机与流程图
3PC的状态转换比2PC复杂。我画了一张图,帮你理清思路。
3.6 代码示例:3PC协调者核心逻辑
下面是一个简化版的3PC协调者实现。实际生产环境要复杂得多,但核心逻辑就是这样。
// 3PC协调者核心逻辑(伪代码)
public class ThreePCCoordinator {
private List<Participant> participants;
private int timeout = 5000; // 超时时间5秒
public boolean executeTransaction(Transaction tx) {
// 第一阶段:CanCommit
if (!canCommit(tx)) {
abortTransaction(tx);
return false;
}
// 第二阶段:PreCommit
if (!preCommit(tx)) {
abortTransaction(tx);
return false;
}
// 第三阶段:DoCommit
return doCommit(tx);
}
private boolean canCommit(Transaction tx) {
for (Participant p : participants) {
boolean result = p.canCommit(tx);
if (!result) {
return false; // 任何一个参与者拒绝,就中止
}
}
return true;
}
private boolean preCommit(Transaction tx) {
for (Participant p : participants) {
boolean result = p.preCommit(tx);
if (!result) {
return false; // 预提交失败,回滚
}
}
return true;
}
private boolean doCommit(Transaction tx) {
boolean allSuccess = true;
for (Participant p : participants) {
boolean result = p.doCommit(tx);
if (!result) {
allSuccess = false; // 记录失败,但继续提交其他参与者
}
}
return allSuccess;
}
}
3.7 什么时候用3PC?
3PC不是银弹。我一般只在以下场景考虑它:
- 对一致性要求高:比如金融交易、订单支付
- 节点数量少:3-5个节点,再多的话网络开销受不了
- 网络相对稳定:频繁超时会导致大量事务被误判
- 能接受一定的复杂度:团队有足够能力维护
如果节点多、网络不稳定,我建议考虑TCC或者Saga。它们虽然一致性弱一些,但可用性和性能好得多。
好了,3PC就聊到这儿。记住它的核心:多一个阶段,多一份自主权。下一章咱们聊聊TCC,那又是另一种思路了。