Go微服务中无法用database/sql的tx.Commit()实现跨服务事务,因其仅作用于本地数据库,对HTTP/gRPC调用无效;正确方案是本地消息表或Saga模式,需状态持久化、幂等控制与补偿可重入。
Go 微服务里没有“提交就能跨服务生效”的分布式事务。database/sql 的 tx.Commit() 只作用于本机数据库连接,对 HTTP/gRPC 调用的库存、支付等服务完全无效——这是绝大多数人踩坑的起点。
常见错误是写个 tx, _ := db.Begin(),然后串行调 http.Post("/inventory/deduct") 和 http.Post("/payment/charge"),最后 tx.Commit()。这根本不是分布式事务:
context.WithValue(ctx, "xid", ...) 只是标记,不是控制;Go 标准库不传播事务上下文到 HTTP header 或 gRPC metadata核心是把“改业务数据”和“发消息”塞进同一个数据库事务,靠定时任务异步投递,状态可查、可重试、可人工干预。
outbox 时,要用 SELECT ... FOR UPDATE SKIP LOCKED 避免重复处理Idempotency-Key header、DB 唯一键或 Redis SETNX 去重Compensate 不是反向操作,而是基于当前状态做安全修正。直接 DELETE FROM orders 或 UPDATE stock SET qty = qty + 10 是高危操作。
立即学习“go语言免费学习笔记(深入)”;
UPDATE payments SET status = 'refunded' WHERE id = ? AND status = 'paid'
*CreateOrderRequest),不能只传 IDsaga_dead_letter 表并告警UPDATE stock SET frozen = frozen + ? WHERE sku_id = ? AND frozen <= ?,而不是先 SELECT 再 UPDATE框架只管协调,不替你写业务逻辑。掉坑往往发生在初始化和重试环节。
Timeout: 5 * time.Second
if exists, _ := s.db.IsStepDone(ctx, gid, "deduct_stock"); exists { return nil }
no available service 或 failed to register branch,本质是注册中心未通或 service.vgroupMapping 配置不匹配最易被忽略的是状态持久化位置:Saga 实例状态绝不能存在内存 map 或 Redis 中——服务重启就丢,整个事务链直接卡死。必须落库,且字段至少包含 gid、status、current_step、data(JSON)。这不是可选项,是保命线。