db.BeginTx在微服务中必然失效,因其仅绑定单个数据库连接,而微服务间数据库天然隔离;需通过Saga补偿、TCC事务日志、异步可靠任务等业务层机制保障最终一致性。

Go 微服务里没法靠 db.BeginTx 跨服务保原子性,这不是配置问题,是架构限制——每个服务连自己的数据库,tx.Commit() 对隔壁服务的 MySQL 或 PostgreSQL 完全没感知。真正要解决的,不是“怎么一起提交”,而是“失败后怎么撤回”。
为什么 db.BeginTx 在微服务里必然失效
标准库的 database/sql.Tx 只绑定单个 *sql.DB 实例,而微服务天然隔离:user-service 的 PostgreSQL 连接和 inventory-service 的 Redis 或 MySQL 之间零关联。常见翻车现场包括:
- 订单写入成功,但库存扣减 HTTP 请求超时,下游压根没收到;日志里只看到
INSERT INTO orders成功,却找不到任何回滚入口 - 支付服务返回 200,但优惠券服务因 panic 回滚了本地事务,结果钱扣了券没发
- 用
context.WithTimeout包裹所有 HTTP 调用,看似有超时,但超时后既没触发补偿、也没记录失败状态,这笔事务彻底“悬空”
这不是 Go 不够强,而是 CAP 下的必然:你选了分区容忍(P)和可用性(A),就得接受一致性(C)只能靠业务层兜底。
Saga 补偿必须用条件更新 + 唯一索引防重
补偿不是“再调一次 refund() 就完事”,网络重试、死信队列重投、人工补单都会导致同一笔操作被执行多次。比如 restoreInventory(sku, 10) 如果没约束,三次调用就多加 30 件库存。
立即学习“go语言免费学习笔记(深入)”;
- 别在补偿函数里先
SELECT status FROM inventory WHERE sku_id = ?再UPDATE——查和更新之间存在竞态,高并发下必出错 - 直接用带状态条件的
UPDATE:UPDATE inventory SET stock = stock + ? WHERE sku_id = ? AND status = 'frozen' - 或建一张
compensation_log(order_id, step_name, created_at)表,用INSERT IGNORE或ON CONFLICT DO NOTHING记录执行痕迹 - HTTP 请求头带上
X-Compensation-ID: order_123_restore_inv,下游用SETNX compensation:order_123_restore_inv 1 EX 3600做秒级去重
TCC 的 Try 阶段必须落盘事务上下文
Try 不是“检查+冻结”就结束,它得把全局事务 ID、分支 ID、参数快照持久化,否则节点宕机后 Confirm/Cancel 根本无从下手。自己写 TCC 协调器最容易漏掉三类边界:
- 悬挂事务:Try 成功但请求没收到响应,协调器以为失败而发 Cancel,结果 Confirm 和 Cancel 同时执行
- 空回滚:Try 没执行,但 Cancel 被误触发,解冻一个根本没冻结的账户
-
幂等缺失:Confirm 接口没校验
WHERE gid = ? AND status = 'trying',重复调用导致余额被扣两次
所以 Try 必须写事务日志表(如 transaction_log(gid, branch_id, status, payload, created_at)),Confirm/Cancel 才能基于该记录做 CAS 更新:UPDATE transaction_log SET status = 'confirmed' WHERE gid = ? AND status = 'trying'。
补偿任务必须异步化 + 独立 worker 扫描
千万别在 HTTP handler 里直接调用补偿逻辑——主链路会卡死、超时、雪崩。你本想修复不一致,结果先搞垮下单接口。
- handler 只负责写一条
outbox_events(event_type, payload, processed)记录,并在同一个tx.Commit()里完成订单创建 - 另起独立 goroutine 或定时 job(如
entgo + cron),轮询SELECT * FROM outbox_events WHERE processed = false LIMIT 100 - 补偿任务本身设
max_retry = 3+ 指数退避(1s → 2s → 4s),失败记录进saga_records表供人工介入 - 别用
time.AfterFunc做延迟补偿——进程一挂,任务全丢;必须依赖持久化调度器(如asynq.ProcessAt)
最常被忽略的一点:补偿不是“越快越好”,而是“必须可追溯”。每一步的 gid、step_name、status 都得进 DB,否则故障时连哪步卡住都查不出来。


















