3、两阶段提交(2PC)详解:2PC执行流程、协调者与参与者角色、2PC的优缺点分析

聊到分布式事务,2PC 绝对是个绕不开的话题。

我记得刚接触微服务那会儿,第一次遇到跨库数据不一致的问题,查了一圈资料,发现大家都在提两阶段提交。当时觉得这名字挺唬人,后来真正用上了才发现——嗯,它确实是个好方案,但也是个「有脾气」的方案。

今天咱们就把 2PC 掰开揉碎,好好聊一聊。

什么是两阶段提交?

两阶段提交,英文叫 Two-Phase Commit,简称 2PC。

说白了,它就是一个「协调者」带着一群「参与者」做决策的过程。大家先各自准备,确认自己能不能干;等所有人都说「我能干」了,协调者再一声令下:「动手!」

如果有人说「我干不了」,那就全员回滚,谁也别动。

你想想看,这像不像团队开会?先问大家有没有困难,都说没问题了,再开始执行。有人掉链子,那就重新讨论方案。

两个核心角色

2PC 里只有两个角色,但分工非常明确:

  • 协调者(Coordinator):全局的决策者。负责发起事务、收集投票、决定提交还是回滚。
  • 参与者(Participant):实际执行操作的节点。负责执行本地事务,并反馈自己的状态。

我在项目中遇到过一种情况:协调者挂了,参与者全都在等指令。那叫一个尴尬。所以后来我习惯给协调者做高可用,至少得有个备胎。

2PC 的执行流程

两阶段提交,顾名思义,分两个阶段走。

第一阶段:投票阶段(Voting Phase)

这个阶段,协调者会问所有参与者:「你们准备好了吗?」

  1. 协调者向所有参与者发送 prepare 请求。
  2. 每个参与者收到请求后,开始执行本地事务,但先不提交。
  3. 参与者把执行结果写入日志,然后回复协调者:YesNo

这里有个细节要注意:参与者一旦回复 Yes,就意味着它承诺自己一定能提交。哪怕后面协调者挂了,它也得等着。

⚠️ 我曾经踩过的坑: 参与者回复 Yes 后,如果本地资源被锁住太久,其他请求就会超时。所以超时时间一定要设好,别让锁一直占着。

第二阶段:提交阶段(Commit Phase)

协调者收集完所有参与者的回复后,开始做决策:

  • 全部回复 Yes:协调者发送 commit 指令,所有参与者提交事务。
  • 有任何一个 No:协调者发送 rollback 指令,所有参与者回滚事务。

参与者收到指令后,执行提交或回滚,然后回复 ack。协调者收到所有 ack 后,事务结束。

你可能会问:如果协调者发送 commit 时,某个参与者没收到怎么办?

嗯,这就是 2PC 的一个痛点——阻塞问题。参与者一旦进入等待状态,就只能死等协调者的指令。如果协调者挂了,参与者就卡住了。

2PC 的优缺点分析

任何方案都有两面性。2PC 也不例外。

优点

  • 原理简单:两个阶段,逻辑清晰,容易理解。
  • 强一致性:要么全部提交,要么全部回滚,数据不会出现中间状态。
  • 支持多种资源:数据库、消息队列、文件系统,只要实现了 2PC 协议,都能参与。

我个人觉得,2PC 最大的价值在于它的「确定性」。你不需要担心数据对不上的问题,只要协调者不挂,结果就是一致的。

缺点

  • 同步阻塞:参与者等待协调者指令期间,会锁住资源。高并发场景下,性能会急剧下降。
  • 单点故障:协调者一旦宕机,整个事务就卡住了。参与者只能等,直到协调者恢复。
  • 数据不一致风险:协调者发送 commit 后自己挂了,部分参与者收到了指令并提交,部分没收到。这时候数据就不一致了。
  • 性能开销大:两轮网络通信,加上日志写入,延迟比单机事务高不少。
💡 我的经验之谈: 2PC 适合低频、短事务的场景。比如金融转账、订单支付这种对一致性要求极高、并发量不大的业务。千万别拿它去做秒杀系统,那会把自己玩死。

一个简单的 2PC 示例

假设我们有两个数据库:账户库和订单库。用户下单时,需要同时扣减账户余额和创建订单。

// 协调者伪代码
function twoPhaseCommit() {
    // 第一阶段:准备
    boolean accountReady = accountDB.prepare();
    boolean orderReady = orderDB.prepare();

    // 第二阶段:决策
    if (accountReady && orderReady) {
        accountDB.commit();
        orderDB.commit();
    } else {
        accountDB.rollback();
        orderDB.rollback();
    }
}

// 参与者伪代码
function prepare() {
    // 开启本地事务,执行操作但不提交
    beginTransaction();
    executeSQL();
    writeLog("prepared");
    return true;
}

function commit() {
    // 提交本地事务
    commitTransaction();
    writeLog("committed");
}

function rollback() {
    // 回滚本地事务
    rollbackTransaction();
    writeLog("rolled back");
}

这段代码看起来简单,但实际生产环境里,你还要考虑网络超时、协调者宕机恢复、日志重放等问题。嗯,细节决定成败。

什么时候该用 2PC?

说实话,现在很多场景已经不太推荐直接用 2PC 了。因为它的阻塞问题太致命。

但如果你遇到以下情况,2PC 仍然是个不错的选择:

  • 业务对一致性要求极高,不能容忍任何数据不一致。
  • 事务涉及的节点数量少(2~3个),且网络稳定。
  • 事务执行时间短,锁资源的时间可以接受。
📌 避坑指南: 我曾经在一个项目里用 2PC 管理 5 个数据库节点,结果频繁出现超时和死锁。后来改成 TCC 模式,问题才解决。所以节点数超过 3 个时,我建议你慎重考虑 2PC。

总结

2PC 是分布式事务的「元老级」方案。它简单、可靠,但也很「重」。

理解 2PC 的核心思想,对你后面学习 TCC、Saga 这些方案会很有帮助。毕竟,很多高级方案都是在 2PC 的基础上做了优化。

下一节,我会聊聊 2PC 在实际落地时的一些坑,以及如何用「超时 + 补偿」来缓解它的阻塞问题。咱们到时候见。