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

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

说实话,我第一次接触3PC时,觉得它就是把2PC的"二段"拆成了"三段",没啥新鲜的。直到我在一个跨境支付系统里真正用上它,才体会到这多出来的一步有多关键。

3.1 三阶段提交的核心流程

3PC把整个提交过程拆成了三个阶段:CanCommitPreCommitDoCommit

你想想看,2PC最大的痛点是什么?是协调者挂了,参与者只能干等。3PC引入了一个"中间状态",让系统有了回旋余地。

核心思想:在真正提交之前,先让所有参与者确认"我能干",再让所有人准备"我要干了",最后才真正"干"。每一步都有超时机制,不会死锁。

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

协调者问所有参与者:"我要提交事务了,你们能行吗?"

参与者检查自身状态,比如资源是否充足、锁是否能拿到。能就回复YES,不能就回复NO。

这一步其实很轻量。我在项目中遇到过一种情况:某个参与者明明能提交,但因为网络延迟,回复超时了。协调者直接判定为NO,整个事务就回滚了。嗯,这里要注意——超时就是NO,这是3PC的默认策略。

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

如果所有参与者都回复YES,协调者就发PreCommit指令。

参与者收到后,开始执行事务操作,但不提交。只是把数据写到undo/redo日志里,把资源锁住。

这一步完成后,参与者会回复ACK。

个人经验:PreCommit阶段是3PC的精髓。它让所有参与者进入"待提交"状态,即使协调者挂了,参与者也能根据超时自行决定下一步。我曾经在一个支付系统中,就因为PreCommit的超时机制,避免了2PC中常见的"协调者宕机导致全局死锁"问题。

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

协调者收到所有ACK后,发DoCommit指令。

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

如果某个参与者没收到DoCommit(比如网络断了),它会等待超时,然后自动提交。为什么?因为PreCommit已经确认了所有参与者都能提交,所以超时后默认提交是安全的。

3.2 3PC如何解决2PC的问题

2PC有两个硬伤:单点阻塞脑裂风险。3PC是怎么解决的?

问题 2PC的表现 3PC的改进
协调者宕机 参与者阻塞,一直等待 参与者超时后自动提交或回滚
网络分区 参与者无法达成一致 PreCommit阶段确认了所有参与者的能力
资源锁定时间 从准备到提交一直锁着 CanCommit阶段不锁资源,PreCommit才锁
容错能力 几乎为零 引入了超时和自动决策机制

说白了,3PC的核心改进就是:引入超时机制 + 增加中间状态。让系统在异常情况下能"自己拿主意",而不是死等协调者。

注意:3PC并没有完全解决脑裂问题。如果网络分区发生在PreCommit之后,一部分参与者收到了DoCommit,另一部分没收到,那么没收到的那部分会超时自动提交。结果就是——所有参与者都提交了,但数据可能不一致。这种情况虽然概率极低,但确实存在。

3.3 3PC的代码实现思路

咱们来看看3PC的核心代码怎么写。我习惯用状态机来管理事务状态。

// 事务状态枚举
enum TransactionState {
    INIT,
    CAN_COMMIT,
    PRE_COMMIT,
    DO_COMMIT,
    ABORT
}

// 协调者核心逻辑
class Coordinator {
    private Map<String, TransactionState> participantStates;
    private ScheduledExecutorService scheduler;
    
    // 第一阶段:CanCommit
    public boolean canCommit(String transactionId) {
        // 向所有参与者发送CanCommit请求
        List<Boolean> responses = broadcastCanCommit(transactionId);
        
        // 全部回复YES才继续
        if (responses.stream().allMatch(r -> r)) {
            participantStates.put(transactionId, TransactionState.CAN_COMMIT);
            return true;
        }
        // 有NO或超时,直接回滚
        abort(transactionId);
        return false;
    }
    
    // 第二阶段:PreCommit
    public boolean preCommit(String transactionId) {
        // 发送PreCommit指令
        List<Boolean> acks = broadcastPreCommit(transactionId);
        
        if (acks.stream().allMatch(r -> r)) {
            participantStates.put(transactionId, TransactionState.PRE_COMMIT);
            return true;
        }
        // 有参与者预提交失败,回滚
        abort(transactionId);
        return false;
    }
    
    // 第三阶段:DoCommit
    public void doCommit(String transactionId) {
        // 发送最终提交指令
        broadcastDoCommit(transactionId);
        participantStates.put(transactionId, TransactionState.DO_COMMIT);
    }
}

// 参与者核心逻辑
class Participant {
    private TransactionState state;
    private Timer timer;
    
    // 处理CanCommit请求
    public boolean handleCanCommit(String transactionId) {
        // 检查资源是否充足
        if (checkResources()) {
            state = TransactionState.CAN_COMMIT;
            return true;
        }
        return false;
    }
    
    // 处理PreCommit请求
    public boolean handlePreCommit(String transactionId) {
        // 执行预提交操作,写入日志
        writeUndoRedoLog();
        state = TransactionState.PRE_COMMIT;
        
        // 启动超时定时器
        startTimeoutTimer(() -> {
            // 超时后自动提交
            if (state == TransactionState.PRE_COMMIT) {
                doCommit(transactionId);
            }
        });
        return true;
    }
    
    // 处理DoCommit请求
    public void handleDoCommit(String transactionId) {
        // 正式提交
        commitTransaction();
        state = TransactionState.DO_COMMIT;
        cancelTimeoutTimer();
    }
}

避坑指南:我曾经在实现PreCommit超时逻辑时,把超时时间设得太短。结果网络稍微波动一下,参与者就自动提交了,而协调者还在等ACK。最后导致数据不一致。我的建议是:超时时间至少是网络延迟的3倍,并且要加上随机抖动,避免所有参与者同时超时造成"惊群效应"。

3.4 3PC的适用场景

3PC不是银弹。它适合的场景是:

  • 对一致性要求极高,但能接受少量性能损失
  • 参与者数量可控,一般不超过10个节点
  • 网络环境相对稳定,但需要一定的容错能力

我在金融交易系统中用过3PC,主要是处理跨行转账的"准备-确认-提交"流程。说实话,性能确实不如2PC,但胜在稳定。你想想看,银行转账少个几毫秒无所谓,但要是因为协调者宕机导致资金卡住,那麻烦就大了。

总结一下:3PC用"多一步确认"换来了"更好的容错性"。它没有完全解决分布式事务的所有问题,但在实际工程中,比2PC靠谱得多。如果你在做一个对一致性要求极高的系统,3PC值得考虑。

3PC三阶段提交协议流程图 协调者 参与者1 参与者2 阶段1: CanCommit CanCommit? CanCommit? YES YES 阶段2: PreCommit PreCommit PreCommit 写入undo/redo日志 写入undo/redo日志 ACK ACK 阶段3: DoCommit DoCommit DoCommit 正式提交事务 正式提交事务 注:任何阶段超时,参与者自动提交或回滚
专注资料整理