3、两阶段提交(2PC)详解:2PC执行流程、协调者与参与者角色、2PC的优缺点分析
聊到分布式事务,2PC 绝对是个绕不开的话题。
我记得刚接触微服务那会儿,第一次遇到跨库数据不一致的问题,查了一圈资料,发现大家都在提两阶段提交。当时觉得这名字挺唬人,后来真正用上了才发现——嗯,它确实是个好方案,但也是个「有脾气」的方案。
今天咱们就把 2PC 掰开揉碎,好好聊一聊。
什么是两阶段提交?
两阶段提交,英文叫 Two-Phase Commit,简称 2PC。
说白了,它就是一个「协调者」带着一群「参与者」做决策的过程。大家先各自准备,确认自己能不能干;等所有人都说「我能干」了,协调者再一声令下:「动手!」
如果有人说「我干不了」,那就全员回滚,谁也别动。
你想想看,这像不像团队开会?先问大家有没有困难,都说没问题了,再开始执行。有人掉链子,那就重新讨论方案。
两个核心角色
2PC 里只有两个角色,但分工非常明确:
- 协调者(Coordinator):全局的决策者。负责发起事务、收集投票、决定提交还是回滚。
- 参与者(Participant):实际执行操作的节点。负责执行本地事务,并反馈自己的状态。
我在项目中遇到过一种情况:协调者挂了,参与者全都在等指令。那叫一个尴尬。所以后来我习惯给协调者做高可用,至少得有个备胎。
2PC 的执行流程
两阶段提交,顾名思义,分两个阶段走。
第一阶段:投票阶段(Voting Phase)
这个阶段,协调者会问所有参与者:「你们准备好了吗?」
- 协调者向所有参与者发送
prepare请求。 - 每个参与者收到请求后,开始执行本地事务,但先不提交。
- 参与者把执行结果写入日志,然后回复协调者:
Yes或No。
这里有个细节要注意:参与者一旦回复 Yes,就意味着它承诺自己一定能提交。哪怕后面协调者挂了,它也得等着。
Yes 后,如果本地资源被锁住太久,其他请求就会超时。所以超时时间一定要设好,别让锁一直占着。
第二阶段:提交阶段(Commit Phase)
协调者收集完所有参与者的回复后,开始做决策:
- 全部回复 Yes:协调者发送
commit指令,所有参与者提交事务。 - 有任何一个 No:协调者发送
rollback指令,所有参与者回滚事务。
参与者收到指令后,执行提交或回滚,然后回复 ack。协调者收到所有 ack 后,事务结束。
你可能会问:如果协调者发送 commit 时,某个参与者没收到怎么办?
嗯,这就是 2PC 的一个痛点——阻塞问题。参与者一旦进入等待状态,就只能死等协调者的指令。如果协调者挂了,参与者就卡住了。
2PC 的优缺点分析
任何方案都有两面性。2PC 也不例外。
优点
- 原理简单:两个阶段,逻辑清晰,容易理解。
- 强一致性:要么全部提交,要么全部回滚,数据不会出现中间状态。
- 支持多种资源:数据库、消息队列、文件系统,只要实现了 2PC 协议,都能参与。
我个人觉得,2PC 最大的价值在于它的「确定性」。你不需要担心数据对不上的问题,只要协调者不挂,结果就是一致的。
缺点
- 同步阻塞:参与者等待协调者指令期间,会锁住资源。高并发场景下,性能会急剧下降。
- 单点故障:协调者一旦宕机,整个事务就卡住了。参与者只能等,直到协调者恢复。
- 数据不一致风险:协调者发送
commit后自己挂了,部分参与者收到了指令并提交,部分没收到。这时候数据就不一致了。 - 性能开销大:两轮网络通信,加上日志写入,延迟比单机事务高不少。
一个简单的 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 是分布式事务的「元老级」方案。它简单、可靠,但也很「重」。
理解 2PC 的核心思想,对你后面学习 TCC、Saga 这些方案会很有帮助。毕竟,很多高级方案都是在 2PC 的基础上做了优化。
下一节,我会聊聊 2PC 在实际落地时的一些坑,以及如何用「超时 + 补偿」来缓解它的阻塞问题。咱们到时候见。