Go微服务分布式事务不能依赖db.BeginTx,必须用Saga模式:状态持久化+幂等控制+异步重试。补偿函数需满足幂等性、带原始上下文、先落库再执行,且正向与补偿步骤均须持久化状态并严格逆序执行。

Go 微服务里做分布式事务,不能靠 db.BeginTx 包住多个 HTTP 调用——它只管单库,对其他服务的失败完全无感。真正能落地的,是把补偿逻辑写进业务代码,并用状态持久化+幂等控制+异步重试兜底。
为什么 database/sql.Tx 在跨服务时完全失效
你在一个函数里调用 AService.Deduct() 成功,再调 BService.CreateOrder() 失败,然后执行 tx.Rollback() —— 这个 rollback 只会回滚当前服务本地数据库里的变更,对 A 服务已发生的扣款毫无影响。HTTP 请求不是事务参与者,context.Context 的超时也只作用于本机 SQL 执行,不传导到远程服务。
- 常见错误:把多个
http.Post套在同一个sql.Tx里,以为加了defer tx.Rollback()就安全 - 本质问题:
database/sql设计上就不处理跨数据源协调,这不是配置或版本问题,而是 CAP 约束下的必然 - 后果:订单创建成功但库存没扣、支付成功但优惠券没发,日志里查不到任何“回滚入口”
Saga 模式下补偿函数必须满足的三个硬约束
Saga 不是“写了 CompensateXxx 函数就完事”,而是每个补偿动作都得扛住重试、并发、重启这三重压力。
- 幂等性必须由 SQL 或存储层保证,而不是靠代码
if判断:例如UPDATE payments SET status = 'refunded' WHERE id = ? AND status = 'charged',避免查余额再更新导致竞态 - 补偿操作要带原始上下文:
CompensateTransferOut(ctx, tx, orderId, amount)必须和正向函数参数一致,否则重试时拿不到关键 ID 和金额 - 补偿不能直接发 HTTP:先插入
saga_records(step='compensate_payment', status='pending'),再由独立 worker 轮询执行;否则进程崩溃,补偿就永远丢失
用 select 控制 Saga 流程,别用同步阻塞
跨服务调用天然高延迟,如果每个步骤都用 http.DefaultClient.Do 同步等结果,整个流程会卡死在某一步,既无法超时熔断,也无法并发推进补偿。
立即学习“go语言免费学习笔记(深入)”;
- 正向步骤需监听完成信号、超时信号、取消信号三者之一:用
select配合time.After和ctx.Done() - 补偿触发不能只依赖上一步 panic:要支持异步扫描 + 定时任务,例如查
status = 'pending'且updated_at < now() - 30s的记录 - 失败后补偿顺序必须严格逆序:Dapr 的
saga.AddCompensatingStep默认按添加顺序执行正向步骤,按逆序执行补偿;自己实现时也要显式维护步骤栈
状态持久化是补偿机制可靠性的唯一锚点
所有正向操作和补偿动作的状态,必须落库(或写入 Redis),不能只存在内存或临时变量里。宕机重启后,系统得能从持久化状态里恢复执行位置。
- 推荐建一张
saga_records表,至少含字段:trace_id、step_name、status(pending/succeeded/compensated)、payload(JSON)、created_at、updated_at - 每步正向操作前,先
INSERT INTO saga_records记录为pending;成功后再UPDATE ... SET status = 'succeeded';补偿同理 - 不要用
go func() { amqp.Publish() }()发消息:消息必须和主业务记录一起 commit,例如同一*sql.Tx中插入orders和outbox_events两条记录
补偿机制最易被忽略的点,是补偿动作本身也需要可靠性保障——它不是“尽力而为”的辅助逻辑,而是和正向操作同等重要的业务路径。一旦补偿失败,没有第二道防线。


















