go中gin暴露2pc接口时,/prepare必须真实执行数据库xa prepare或prepare transaction,不可为空函数;需透传tx_id、独立超时控制、返回标准状态码,并持久化事务状态至wal或etcd,确保全局唯一可追溯。

Go里用Gin暴露2PC接口,别把Prepare写成空函数
很多人在Gin里定义 /prepare 接口时,只做日志打印或状态标记,没真正调用数据库的 XA PREPARE 或 PREPARE TRANSACTION。这会导致事务根本没进入 prepared 状态,后续 COMMIT 或 ROLLBACK 都是无效操作。
真实场景下,每个 Prepare() 必须对应一次可落地的预提交:
- MySQL:先
XA START 'tx-ok'→ 执行业务SQL →XA PREPARE 'tx-ok',缺一不可;仅Begin()+Exec()不算 - PostgreSQL:需提前在
postgresql.conf中设max_prepared_transactions > 0,再执行BEGIN TRANSACTION→UPDATE→PREPARE TRANSACTION 'tx-ok' - 自定义服务(如库存)若无原生预提交能力,必须持久化
Try结果到 WAL 或 etcd,并记录唯一tx-id和幂等 key
Gin路由层要隔离事务ID、超时与响应状态
协调者发来的 POST /prepare 请求体里必须带 tx_id 字段,且该值要透传到底层数据库命令(如 XA START 'tx-ok')。不能用随机UUID覆盖原始ID——否则 coordinator 崩溃后无法通过 XA RECOVER 关联悬挂事务。
每个参与者接口需独立控制超时,不能共用 Gin 的全局 context.WithTimeout:
- 在 handler 内部用
context.WithTimeout(ctx, 5*time.Second)包裹 DB 操作 - 返回 HTTP 状态码要区分:成功返回
200 OK,XA PREPARE失败返回409 Conflict(表示资源冲突),网络超时或DB不可达返回504 Gateway Timeout - 响应 body 至少包含
{"tx_id": "tx-ok", "status": "prepared"},便于 coordinator 持久化状态
Commit失败后不轮询=生产事故
POST /commit 接口看似简单,但一旦返回 5xx,该参与者极大概率卡在 prepared 状态。MySQL 会一直持有行锁和连接,PG 会在 pg_prepared_xacts 里长期驻留记录。
必须配套两件事:
- 启动后台 goroutine,每 30 秒查一次
XA RECOVER(MySQL)或SELECT * FROM pg_prepared_xacts(PG),对超时未决的tx-id主动发XA COMMIT或XA ROLLBACK - 提供运维命令入口,比如
go run . recover --txid=tx-ok --force=commit,绕过 coordinator 直接干预 -
Commit()本身必须幂等:数据库原生命令天然满足,但自定义服务的Confirm()要校验请求头里的X-Request-ID去重
Gin中间件不能替代事务状态持久化
有人用 Gin 的中间件在内存 map 里存 map[string]string{"tx-ok": "prepared"},这是危险的。进程重启后状态全丢,coordinator 无法判断该事务是否已 prepare,只能盲目 abort,造成数据不一致。
真正可用的状态存储必须满足两点:
- 写入时机:收到
/prepare成功响应后,立刻落盘(WAL 文件或 etcd);收到/commit后更新为committed - 读取逻辑:coordinator 启动时先从存储加载所有未决
tx-id,对状态为prepared但无 commit 记录的,触发异步清理 - 避免用 Redis 存核心状态——它不是强一致存储,网络分区时可能丢失写入
事务 ID 的全局唯一性和可追溯性不是设计选项,是底线。漏掉这一条,整个 2PC 模块在故障恢复时就失效了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











