3、强一致性方案之3PC:三阶段提交协议详解

聊完2PC,咱们来看看它的升级版——3PC。

说实话,我第一次接触3PC时,心里想的是:「多一个阶段就能解决问题了?」后来在项目中踩过坑才明白,这个「多一阶段」的设计,其实挺巧妙的。

3.1 为什么需要3PC?

2PC有个硬伤:协调者单点故障

我记得有一次线上压测,协调者节点突然挂了。所有参与者都卡在「准备」状态,资源锁着不放。业务方急得跳脚,我们只能手动清理事务。那场面,真叫一个狼狈。

2PC的另一个问题是:参与者太被动。协调者挂了,参与者只能傻等。没人告诉它们该提交还是回滚。

3PC就是来解决这两个问题的。它引入了超时机制,还加了一个「预提交」阶段。说白了,就是给参与者更多自主权。

3.2 三阶段提交的核心流程

3PC把事务提交拆成了三步:CanCommitPreCommitDoCommit

你想想看,这就像项目评审会:先问大家「能不能做?」(CanCommit),再问「准备好了吗?」(PreCommit),最后说「开始干!」(DoCommit)。

核心思想: 把2PC的「准备阶段」拆成两步,引入超时机制,让参与者在超时后能自主决策。

第一阶段:CanCommit(询问阶段)

协调者问所有参与者:「这个事务你们能处理吗?」

参与者只需要检查自身状态:网络通不通?资源够不够?事务能不能执行?

注意,这里不执行任何实际操作,只是做个可行性检查。我见过不少新手把这一步和PreCommit搞混,结果在CanCommit里就锁了资源,导致系统卡死。

  • 协调者发送CanCommit请求
  • 参与者回复Yes/No
  • 如果有任何一个No,协调者就中止事务

第二阶段:PreCommit(预提交阶段)

如果所有参与者都回复Yes,协调者就进入PreCommit。

这一步,参与者要真正执行事务操作,但先不提交。把数据写入undo/redo日志,资源锁住。

嗯,这里有个关键点:参与者开始有超时机制了。如果在一定时间内没收到协调者的DoCommit指令,参与者可以自行中止事务。

我的经验: PreCommit的超时时间要设置合理。太短容易误判,太长又失去了意义。我一般设为网络RTT的3-5倍。

第三阶段:DoCommit(提交阶段)

协调者确认所有参与者都PreCommit成功后,发送DoCommit指令。

参与者收到后,正式提交事务,释放资源。

如果协调者挂了怎么办?参与者会等待超时。超时后,默认执行提交。为什么是提交而不是回滚?因为能走到DoCommit,说明所有参与者都准备好了,提交是更安全的选择。

注意: 3PC的「超时提交」策略并非万无一失。如果协调者在PreCommit之后、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多不少
避坑指南: 我曾经在一个高并发场景下用3PC,结果PreCommit阶段锁资源太多,导致系统吞吐量下降。后来改成「先做CanCommit过滤,再对通过的事务做PreCommit」,效果好了很多。

3.5 3PC的状态机与流程图

3PC的状态转换比2PC复杂。我画了一张图,帮你理清思路。

3PC三阶段提交状态机 协调者状态机 初始 CanCommit PreCommit DoCommit 中止 参与者状态机 初始 就绪 预提交 已提交 中止 超时提交 超时中止 实线:正常流程 | 虚线:超时/异常流程 协调者故障时,参与者在PreCommit阶段超时后默认提交 在就绪阶段超时后默认中止

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;
    }
}
个人建议: 实际项目中,3PC的协调者最好做成无状态的,配合消息队列做持久化。这样即使协调者挂了,也能从消息队列恢复状态。我在一个支付系统中就是这么做的,效果不错。

3.7 什么时候用3PC?

3PC不是银弹。我一般只在以下场景考虑它:

  • 对一致性要求高:比如金融交易、订单支付
  • 节点数量少:3-5个节点,再多的话网络开销受不了
  • 网络相对稳定:频繁超时会导致大量事务被误判
  • 能接受一定的复杂度:团队有足够能力维护

如果节点多、网络不稳定,我建议考虑TCC或者Saga。它们虽然一致性弱一些,但可用性和性能好得多。

好了,3PC就聊到这儿。记住它的核心:多一个阶段,多一份自主权。下一章咱们聊聊TCC,那又是另一种思路了。


专注资料整理