资讯动态

两阶段提交2PC协议详解:从原理到Seata落地实践

发布时间:2026/10/17 15:03:50 来源:尧图企业网站定制
1. 从一次数据不一致事故说起2PC到底在解决什么问题很多同学第一次接触“2PC”这个词是在分布式事务的面试题里背一句“两阶段提交准备阶段加提交阶段”就过去了。但真正在项目里踩过坑的人都知道2PC远不是背概念那么简单。我最早接触它是在一个跨库转账的场景里A库扣钱、B库加钱两个库物理隔离中间没有任何共享事务管理器。当时天真地以为“先扣再增失败了回滚就行”结果一次网络抖动直接导致A库扣了钱、B库没加上账对不上排查了整整一个通宵。这就是2PCTwo-Phase Commit两阶段提交要解决的核心问题在多个独立的资源数据库、消息队列、缓存服务等之间保证一次操作要么全部成功要么全部失败。它本质上是一个分布式原子提交协议把“提交”这个动作拆成两个阶段来完成用协调者Coordinator统一指挥所有参与者Participant。这篇文章适合谁看如果你正在做分库分表、微服务拆分、跨系统数据同步或者你被“分布式事务一致性”这个问题困扰过那这篇内容就是写给你的。我会从协议原理讲到落地实现从参数配置讲到故障排查尽量把我在实际项目里踩过的坑和总结的经验都摊开讲清楚。不管你是刚接触分布式事务的新手还是已经用过Seata、XA、Atomikos的老手都能从中找到可以直接抄作业的部分。需要先明确一点2PC不是一个具体的框架或工具它是一个协议规范。XA是它在数据库层面的工业标准实现Seata的AT模式、TCC模式在某种程度上也借鉴了它的思想。理解2PC是理解所有分布式事务方案的地基。2. 协议核心机制拆解两个阶段到底在干什么2.1 角色划分与整体流程2PC里有两个核心角色这个必须先搞清楚不然后面全是糊涂账。协调者Coordinator全局事务的“总指挥”负责发起投票、收集结果、下达最终指令。它可以是独立的服务比如Seata的TC也可以是嵌入在业务代码里的一个组件。参与者Participant真正执行数据操作的资源节点比如一个MySQL实例、一个RocketMQ的Broker、一个Redis集群。每个参与者只对自己的本地事务负责。整个流程用一句话概括协调者先问所有人“你们能不能提交”所有人都说“能”协调者才说“那就提交”只要有一个人说“不能”协调者就说“全部回滚”。这里有个关键点很多人会忽略第一阶段不是“提交”而是“准备”。参与者在这个阶段做的是“把该锁的资源锁住、该写的日志写好但先不真正生效”。这就像结婚前的订婚仪式彩礼给了、酒席定了但证还没领随时可以反悔。2.2 第一阶段准备阶段Prepare Phase准备阶段也叫投票阶段协调者向所有参与者发送prepare请求参与者收到后执行以下动作开启本地事务执行SQL但不提交。写入undo/redo日志确保后续无论提交还是回滚都有据可查。锁定相关资源比如行锁、表锁防止其他事务干扰。向协调者返回投票结果Yes表示“我准备好了随时可以提交”No表示“我这边出问题了赶紧回滚吧”。这个阶段最要命的是锁。以MySQL的XA为例一个XA事务在prepare之后对应的行锁会一直持有直到协调者下达commit或rollback。如果协调者挂了这些锁就会一直不释放其他事务全部阻塞。我在一个高并发项目里见过因为协调者超时未响应导致某张热点表的行锁堆积了上千个整个服务直接雪崩。提示准备阶段一定要设置合理的超时时间。参与者不能无限等待协调者的指令超时后应该主动回滚并释放锁否则就是给自己埋雷。2.3 第二阶段提交阶段Commit Phase协调者收集完所有参与者的投票后分两种情况处理情况一全部投Yes协调者向所有参与者发送commit指令参与者收到后执行本地事务的提交操作释放锁返回ack。协调者收到所有ack后整个分布式事务才算真正完成。情况二任意一个投No或超时协调者向所有参与者发送rollback指令参与者利用第一阶段写入的undo日志回滚本地事务释放锁返回ack。同样协调者要等所有ack到齐才算结束。这里有个细节协调者的决策一旦做出就是不可逆的。哪怕它在发出commit指令后自己宕机了参与者也不能自作主张回滚必须等协调者恢复后重新下达指令。这就是为什么协调者需要做持久化——它必须把“我决定提交了”这件事记到磁盘上重启后才能知道该继续发commit还是rollback。2.4 为什么是“两阶段”而不是“一阶段”有人会问为什么不直接让协调者收集完所有参与者的“预执行结果”然后一次性提交非要分两步原因在于网络的不确定性。如果只有一阶段协调者发出“执行”指令后部分参与者执行成功、部分失败协调者自己也不知道到底该整体提交还是整体回滚。两阶段的价值在于把“决策”和“执行”分离。第一阶段收集信息、锁定资源第二阶段根据确定的信息做最终决策。这样即使第二阶段出现网络问题协调者也有明确的依据去重试或补偿。用生活化的例子解释公司团建要订机票行政先问所有人“这个时间行不行”准备阶段所有人都确认后行政才统一付款出票提交阶段。如果先付款再问有人时间冲突就麻烦了。两阶段就是把“确认”和“付款”分开降低整体失败的概率。3. 落地实现方案从XA到Seata的选型对比3.1 XA规范数据库层面的标准实现XA是X/Open组织定义的分布式事务规范MySQL、Oracle、PostgreSQL等主流数据库都支持。它定义了一套接口让数据库作为参与者接入2PC流程。在MySQL里XA事务的用法大致如下-- 第一阶段准备 XA START transaction_id_001; UPDATE account SET balance balance - 100 WHERE user_id 1; XA END transaction_id_001; XA PREPARE transaction_id_001; -- 第二阶段提交或回滚 XA COMMIT transaction_id_001; -- 或者 XA ROLLBACK transaction_id_001;看起来很简单但实际用起来有几个硬伤性能差XA事务在prepare之后锁不释放并发能力急剧下降。有测试数据显示XA事务的TPS通常只有普通事务的十分之一甚至更低。协调者单点应用层需要自己实现协调者逻辑一旦协调者宕机整个事务悬而未决。数据库兼容性不同数据库对XA的支持程度不一样MySQL的XA实现还有一些已知的bug比如崩溃恢复后可能出现“悬空事务”。所以XA适合什么场景低频、短事务、对一致性要求极高、对性能不敏感的场景。比如银行日终对账、跨库的批量数据迁移。高并发的互联网业务基本不会直接用XA。3.2 Seata AT模式互联网场景的改良版2PCSeata是阿里开源的分布式事务框架它的AT模式本质上是一个改进版的2PC。和XA最大的区别在于AT模式在第一阶段就提交本地事务通过undo log来实现回滚。具体流程第一阶段执行SQL同时记录before image和after image到undo log表然后直接提交本地事务。这样锁很快就释放了性能大幅提升。第二阶段如果全局事务提交异步删除undo log如果全局事务回滚根据undo log生成反向SQL进行补偿。这个设计的好处是把锁的持有时间从“整个分布式事务”缩短到“单个本地事务”并发能力接近普通事务。代价是隔离性变弱——在全局事务提交之前其他事务可能读到中间状态的数据也就是所谓的“脏读”。Seata通过全局锁来缓解这个问题在提交本地事务之前先向TC事务协调者申请全局锁拿到锁才能提交。这样其他事务在修改同一行数据时会被阻塞保证了写隔离。但读隔离仍然需要应用层自己控制比如用SELECT FOR UPDATE。注意Seata AT模式适合“写少读多”或者“冲突不激烈”的场景。如果热点数据频繁跨服务修改全局锁会成为瓶颈这时候要考虑TCC模式或者业务层面的规避方案。3.3 自研协调者的关键设计点有些团队会选择自研2PC协调者这时候有几个设计点必须考虑清楚设计点方案选择理由协调者高可用基于Raft/Paxos做集群避免单点故障导致事务悬挂事务日志存储独立DB或本地文件同步复制崩溃恢复时能重建事务状态超时策略分级超时准备超时、提交超时防止参与者无限等待幂等处理每个指令带唯一事务ID序列号网络重试时避免重复执行补偿机制定时扫描悬挂事务人工介入处理极端情况下的数据不一致自研的成本很高除非你有非常特殊的业务需求否则优先考虑成熟框架。Seata、Atomikos、Narayana都是经过生产验证的选择。4. 实操过程手把手搭建一个可运行的2PC示例4.1 环境准备与依赖配置这里我用一个模拟的跨库转账场景来演示。两个MySQL实例模拟两个不同的数据库一个协调者服务。为了贴近实际我用Spring Boot Seata来实现。先看依赖dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.7.0/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependencySeata的配置分两部分TCServer端和TM/RMClient端。TC需要独立部署Client端在应用里配置。TC端的配置文件application.ymlserver: port: 8091 spring: application: name: seata-server seata: config: type: file store: mode: db db: datasource: druid url: jdbc:mysql://127.0.0.1:3306/seata?useSSLfalse user: root password: your_password driver-class-name: com.mysql.cj.jdbc.DriverClient端的配置seata: enabled: true application-id: transfer-service tx-service-group: my_tx_group service: vgroup-mapping: my_tx_group: default grouplist: default: 127.0.0.1:8091 transport: type: TCP server: NIO heartbeat: true4.2 核心代码实现与参数说明业务代码里用GlobalTransactional注解标记全局事务的入口Service public class TransferService { Autowired private AccountMapper accountMapper; GlobalTransactional(name transfer-tx, timeoutMills 30000, rollbackFor Exception.class) public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) { // 扣款 accountMapper.decreaseBalance(fromUserId, amount); // 模拟远程调用另一个服务 remoteService.increaseBalance(toUserId, amount); } }几个关键参数的解释timeoutMills 30000全局事务超时时间超过30秒TC会主动回滚。这个值要根据业务链路长度来定太短容易误杀太长会导致锁持有过久。rollbackFor Exception.class指定哪些异常触发回滚。默认只回滚RuntimeException和Error受检异常不会触发回滚这个坑很多人踩过。name全局事务名称方便在TC端做监控和排查。每个参与方的数据库需要建一张undo_log表CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) NOT NULL, xid varchar(100) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int(11) NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid,branch_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表是AT模式的核心回滚时全靠它。rollback_info字段存的是序列化后的before image和after image数据量可能很大建议单独放在一个表空间里。4.3 完整执行流程与现场记录启动TC、两个业务服务后发起一次转账请求。我在日志里加了trace完整流程如下[TM] 开启全局事务生成XID: 192.168.1.100:8091:123456 [TM] 向TC注册全局事务 [RM-扣款服务] 收到分支事务注册请求XID123456 [RM-扣款服务] 执行SQL: UPDATE account SET balance balance - 100 WHERE user_id 1 [RM-扣款服务] 记录undo_log: before{balance:1000}, after{balance:900} [RM-扣款服务] 向TC申请全局锁: tableaccount, pk1 [RM-扣款服务] 拿到全局锁提交本地事务 [RM-扣款服务] 向TC报告分支事务状态: PhaseOne_Done [RM-加款服务] 收到分支事务注册请求XID123456 [RM-加款服务] 执行SQL: UPDATE account SET balance balance 100 WHERE user_id 2 [RM-加款服务] 记录undo_log: before{balance:500}, after{balance:600} [RM-加款服务] 向TC申请全局锁: tableaccount, pk2 [RM-加款服务] 拿到全局锁提交本地事务 [RM-加款服务] 向TC报告分支事务状态: PhaseOne_Done [TM] 所有分支事务完成向TC发起全局提交 [TC] 通知所有RM异步删除undo_log [TC] 释放全局锁 [TM] 全局事务提交完成如果加款服务执行失败流程会变成[RM-加款服务] 执行SQL失败向TC报告: PhaseOne_Failed [TC] 收到失败报告决定全局回滚 [TC] 通知扣款服务执行回滚 [RM-扣款服务] 读取undo_log生成反向SQL: UPDATE account SET balance 1000 WHERE user_id 1 [RM-扣款服务] 执行反向SQL删除undo_log [TC] 释放全局锁 [TM] 全局事务回滚完成整个过程中最关键的是undo_log的写入必须在本地事务提交之前完成否则回滚时没有依据。Seata通过数据源代理自动完成这个动作不需要业务代码显式处理。5. 常见问题与排查技巧实录5.1 事务悬挂最隐蔽的数据不一致现象某个分支事务在第一阶段执行成功并提交了本地事务但由于网络问题TC没有收到它的状态报告。TC超时后决定全局回滚向该分支发送回滚指令。但此时该分支的undo_log可能已经被异步删除了导致回滚失败数据不一致。排查思路检查TC端的事务状态表global_table和branch_table看是否有长期处于PhaseOne_Done但未收到二阶段指令的记录。检查各RM的undo_log表看是否有log_status0未删除但对应全局事务已经回滚的记录。对比业务数据和undo_log中的before image确认是否发生了不一致。解决方案TC端配置合理的超时时间避免过早触发回滚。RM端实现幂等回滚逻辑即使undo_log被删除也能通过业务层面的补偿机制恢复数据。对于关键业务增加对账任务定时扫描不一致数据并告警。提示事务悬挂的本质是“二阶段指令丢失”。解决思路要么是让TC可靠地重发指令要么是让RM能够处理重复指令。两者结合效果最好。5.2 全局锁冲突高并发下的性能瓶颈现象多个全局事务同时修改同一行数据后到的请求在申请全局锁时被阻塞响应时间飙升严重时触发超时回滚。排查思路查看TC端的全局锁表lock_table看哪些行的锁等待时间最长。分析业务SQL确认是否存在热点数据跨服务修改的情况。用压测工具模拟并发观察TPS和RT的变化曲线。解决方案方案适用场景代价业务拆分避免热点热点数据可以按用户/时间分片改造成本高改用TCC模式业务能接受手动实现try/confirm/cancel开发工作量大异步化消息队列对实时一致性要求不高最终一致性有延迟本地事务定时补偿跨服务操作可以延迟执行实现复杂需处理幂等我在一个订单场景里遇到过类似问题多个用户同时抢购同一商品库存服务成为全局锁的瓶颈。最后的方案是把库存扣减改成Redis原子操作异步落库绕开了2PC的全局锁。5.3 常见问题速查表问题现象可能原因快速排查方法解决方向事务一直不提交TC宕机或网络不通检查TC服务状态和网络连通性恢复TC配置集群回滚后数据仍不一致undo_log丢失或损坏对比undo_log和业务数据人工补偿对账全局锁等待超时热点数据并发修改查看lock_table等待队列业务拆分或换模式分支事务重复执行网络重试导致指令重复检查事务ID和序列号实现幂等处理TC重启后事务丢失未配置持久化存储检查store.mode配置改用DB存储模式5.4 几个容易被忽略的细节第一undo_log的清理策略。Seata默认是异步删除但如果TC一直没发二阶段指令undo_log会一直堆积。建议配置定时任务清理超过一定时间的孤儿undo_log同时记录告警。第二数据库连接池的配置。2PC场景下一个全局事务会占用多个数据库连接如果连接池太小容易出现“拿不到连接”导致的超时。建议把连接池的最大连接数调大同时设置合理的等待超时。第三跨服务的超时传递。全局事务的超时时间要大于所有分支事务的超时时间之和否则会出现“全局事务已超时回滚但某个分支还在执行”的情况。这个在链路较长的微服务调用中特别容易出问题。第四不要在高频写场景用AT模式。AT模式的全局锁虽然比XA轻量但仍然是串行化的。如果业务本身写冲突就很激烈2PC只会让情况更糟。这种场景应该优先考虑最终一致性方案。6. 适用边界与替代方案思考2PC不是银弹它有明确的适用边界。我个人的经验是强一致、短事务、低并发的场景可以用最终一致、长事务、高并发的场景要慎用。具体来说以下几种情况不建议用2PC事务链路超过5个参与者协调开销太大。单个事务执行时间超过1秒锁持有时间过长。参与者包含非事务型资源如HTTP接口、文件系统无法保证回滚。业务对可用性的要求高于一致性宁可最终一致也不要阻塞。替代方案方面TCC模式通过业务层面的try/confirm/cancel来实现隔离性更好但开发成本高本地消息表定时补偿适合异步场景Saga模式适合长流程业务。选择哪种取决于业务对一致性、性能、开发成本的权衡。我在实际项目里的体会是能不用分布式事务就不用。很多所谓的“跨库操作”其实可以通过业务设计来规避——比如把相关数据放在同一个库、用事件驱动代替同步调用、用对账兜底代替强一致。2PC应该是最后的选择而不是第一选择。最后分享一个小技巧如果你不得不用2PC一定要在TC端配置完善的监控和告警。重点关注三个指标——全局事务成功率、平均执行时间、悬挂事务数量。这三个指标一旦异常说明系统已经在出问题的边缘了。提前发现比事后补救重要得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑