解决Golang微服务中的分布式事务问题

作者:袖梨 2026-07-08
Go微服务中无法用database/sql的tx.Commit()实现跨服务事务,因其仅作用于本地数据库,对HTTP/gRPC调用无效;正确方案是本地消息表或Saga模式,需状态持久化、幂等控制与补偿可重入。

Go 微服务里没有“提交就能跨服务生效”的分布式事务。database/sql 的 tx.Commit() 只作用于本机数据库连接,对 HTTP/gRPC 调用的库存、支付等服务完全无效——这是绝大多数人踩坑的起点。

为什么不能用 database/sql 的 tx.Commit() 跨服务

常见错误是写个 tx, _ := db.Begin(),然后串行调 http.Post("/inventory/deduct")http.Post("/payment/charge"),最后 tx.Commit()。这根本不是分布式事务:

  • 订单创建成功、库存扣减失败、支付却已发起 → 数据永久不一致
  • context.WithValue(ctx, "xid", ...) 只是标记,不是控制;Go 标准库不传播事务上下文到 HTTP header 或 gRPC metadata
  • 所谓“手动两阶段提交”若没持久化状态、没重启恢复能力,就是伪协议,服务一重启就卡死在中间态

本地消息表(Outbox)是最可控的落地方式

核心是把“改业务数据”和“发消息”塞进同一个数据库事务,靠定时任务异步投递,状态可查、可重试、可人工干预。

  • outbox 表必须和业务表同库同实例,否则无法保证原子写入
  • INSERT 必须紧跟在业务 SQL 后面,不能用 goroutine 异步包裹
  • 轮询 goroutine 读 outbox 时,要用 SELECT ... FOR UPDATE SKIP LOCKED 避免重复处理
  • 下游接口必须幂等:用 Idempotency-Key header、DB 唯一键或 Redis SETNX 去重
  • 轮询间隔建议 1–5 秒:太短压 DB,太长影响延迟

Saga 模式里补偿函数怎么写才不翻车

Compensate 不是反向操作,而是基于当前状态做安全修正。直接 DELETE FROM ordersUPDATE stock SET qty = qty + 10 是高危操作。

立即学习“go语言免费学习笔记(深入)”;

  • 补偿前必须查状态:例如 UPDATE payments SET status = 'refunded' WHERE id = ? AND status = 'paid'
  • 补偿函数签名要带原始请求快照(如 *CreateOrderRequest),不能只传 ID
  • 补偿里避免远程调用;超时设为 ≤3s,失败必须写入 saga_dead_letter 表并告警
  • Do 接口要支持空重试:UPDATE stock SET frozen = frozen + ? WHERE sku_id = ? AND frozen <= ?,而不是先 SELECT 再 UPDATE

DTM 或 Seata-Go 接入前必须确认的三件事

框架只管协调,不替你写业务逻辑。掉坑往往发生在初始化和重试环节。

  • dtmcli 客户端默认 1 秒超时,而 DTM server 启动慢或 Docker DNS 解析延迟会导致 panic;务必显式设置 Timeout: 5 * time.Second
  • Confirm/Cancel 接口被重复调用是常态,每个 handler 开头必须查 DB 判断是否已执行:if exists, _ := s.db.IsStepDone(ctx, gid, "deduct_stock"); exists { return nil }
  • Seata-Go 启动失败常见报错是 no available servicefailed to register branch,本质是注册中心未通或 service.vgroupMapping 配置不匹配

最易被忽略的是状态持久化位置:Saga 实例状态绝不能存在内存 map 或 Redis 中——服务重启就丢,整个事务链直接卡死。必须落库,且字段至少包含 gidstatuscurrent_stepdata(JSON)。这不是可选项,是保命线。

相关文章

精彩推荐