Go中实现2PC的核心是让每个Participant真实落地XA或PREPARE语义,协调者必须容忍网络分区与部分失败,而非仅模拟接口;需设置独立超时、持久化响应状态、处理悬挂事务并保障事务ID全局唯一可追溯。

直接上结论:Go里实现两阶段提交(2PC)模块,核心不是写一堆接口和状态机,而是把 Prepare、Commit、Rollback 三件事在每个参与者里真正落地,并让协调者能容忍网络分区和部分失败。否则写出来的只是玩具。
Go中实现Participant必须处理真实数据库的XA或PREPARE语义
很多示例代码用 fmt.Printf 模拟 Prepare(),这完全没意义。真实场景下,每个 Participant 必须对应一个可执行预提交的后端资源:
- MySQL 需调用
XA START→UPDATE ...→XA PREPARE,事务状态才进入 prepared;仅Begin+Commit不算 - PostgreSQL 要用
BEGIN TRANSACTION→UPDATE ...→PREPARE TRANSACTION 'txid',且必须提前设max_prepared_transactions > 0 - 自定义服务(如库存服务)若不支持原子预提交,就得自己实现幂等
Try+ 日志持久化,此时已脱离纯2PC范畴
Coordinator不能假设所有Prepare都返回成功
真实网络环境下,Prepare() 可能超时、返回错误、甚至无响应。协调者必须做三件事:
- 对每个
Participant设置独立超时(比如 5s),不能共用一个 context deadline - 收到任意一个
false或 error,立即触发Rollback(),但要逐个发,不能跳过已超时的参与者 - 记录每个参与者的响应状态到持久化存储(如 etcd 或本地 WAL 文件),否则 coordinator 崩溃后无法恢复决策
Commit阶段失败会导致悬挂事务(hanging transaction)
这是2PC最危险的坑:所有 Prepare 成功,但某个 Commit() 失败(比如 DB 连接断了),该参与者会卡在 prepared 状态,长期持有锁和连接。
立即学习“go语言免费学习笔记(深入)”;
必须配套做两件事:
- 在
Commit()失败后,启动异步清理协程,定期轮询该参与者是否仍处于 prepared 状态(MySQL 查XA RECOVER,PG 查pg_prepared_xacts) - 提供手动干预命令,比如
go run . recover --txid=tx-ok,强制COMMIT PREPARED或ROLLBACK PREPARED - 避免在
Commit()阶段做任何非幂等操作——数据库原生命令天然幂等,但自定义服务的Confirm()必须带唯一请求 ID 和去重逻辑
别忽略XA事务ID的全局唯一性和可追溯性
所有参与者用同一个事务标识符(如 tx-ok)才能在崩溃后被关联起来。这个 ID 不能是随机字符串,而应满足:
- 包含时间戳 + 服务实例标识,确保跨进程不重复
- 作为 SQL 注释透传到每条
UPDATE语句里(例如/* txid=tx-ok */ UPDATE ...),便于事后审计 - 写入协调者日志时,必须同步 fsync 到磁盘,否则 coordinator crash 后连哪笔事务卡住了都不知道
真正难的从来不是写完 Coordinator.Execute(),而是让每个 Participant 在 MySQL/PG/自研服务里都稳住 prepared 状态,并在各种网络抖动、进程 kill、磁盘满的情况下,还能靠日志+轮询+人工指令把事务收尾干净。


















