go 微服务中唯一可落地的分布式事务方案是 saga,需自行实现正向步骤与对称补偿、状态持久化及事件驱动编排,dtm/dapr 仅简化协调器部分,不替代业务逻辑设计。

Go 原生根本不支持分布式事务,别试图用 database/sql 或 gorm.Transaction 跨服务“套壳”
Go 标准库和所有主流 ORM(gorm、sqlx、ent)只管单数据库 ACID,tx.Commit() 对 HTTP 调用的库存服务或支付服务完全无效。常见错误是写个 defer tx.Rollback() 然后串行调 http.Post,结果订单创建成功、库存扣减失败、支付已发起——没人 rollback 任何远端操作。
- Context 透传
XID或事务 ID 不等于事务控制;它只是追踪标记,不能替代协调逻辑 - 所谓“手动两阶段提交”靠 goroutine + channel 实现,本质是伪 2PC:没日志持久化、无超时恢复、节点重启即丢状态
- 硬上 XA 协议?
go-sql-driver/mysql不实现XA START/END/COMMIT接口,数据库端开了也没用
Saga 是 Go 微服务里唯一可落地的方案,但必须自己搭骨架,没有开箱即用的库
你不会找到一个 go get github.com/golang/saga 就能跑通的包。Saga 的核心是「正向步骤 + 对称补偿」+「状态持久化」+「事件驱动编排」,三者缺一不可。DTM、Dapr 这类工具只是帮你省掉协调器部分,Do/Undo 接口仍得你亲手写。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 每个子事务(如
/inventory/deduct)必须配一个独立、幂等的补偿接口(如/inventory/release),路径不能是同一 handler 加 query 参数区分 - 状态必须落库:
saga_instances表至少含id、status(pending/compensating/finished)、current_step、data(JSON) - 别用内存 map 或 Redis 存状态——服务重启就丢,Saga 实例直接卡死在中间态
-
Do接口要支持空重试:UPDATE stock SET frozen = frozen + ? WHERE sku_id = ? AND frozen ,而不是先查再更新
补偿函数不是“反向操作”,而是“状态修正”,且必须可重试、防双花
写 CompensateCreateOrder() 去 DELETE FROM orders 是典型错误——订单可能已被物流服务引用,删不掉,还破坏外键。正确做法是 UPDATE orders SET status = 'canceled' WHERE id = ? AND status = 'created'。
- 补偿必须带前置状态校验,避免“已退款又退一次”:
UPDATE payments SET refunded = true WHERE id = ? AND refunded = false - 网络失败时补偿会重试,所以
RefundPayment()必须能处理“退款中”“已退完”“支付不存在”三种响应,不能一错就 panic - 不要在补偿里做远程调用或复杂计算;它应该就是一条原子 SQL 或轻量 HTTP 请求,超时设为
3s内 - 补偿失败不能静默跳过:要写入
saga_dead_letter表,记录error_message并发告警,留人工兜底入口
Dapr 或 DTM 可降低协调成本,但它们不替你写业务逻辑,反而放大设计缺陷
用 Dapr 启动 --enable-saga,或 DTM 注册 steps 切片,只是把“谁来调、按什么顺序、超时多久”交给边车或服务端。一旦你的 Do 不幂等、Undo 不防重、URL 写成相对路径,整个链路立刻崩。
- DTM 要求
Action和CompensateURL 必须是绝对地址(http://inventory-svc:8080/deduct),写成/deduct就真去请求 localhost - Dapr Saga 中,若某步返回非
2xx或响应体没{"result": "SUCCESS"},它会立即触发前序补偿——但如果你的ConfirmPayment返回了503却实际扣款成功,就会造成资损 - 所有接口必须接受并透传
saga_id和step_id,方便日志串联;别依赖context.WithValue,RPC 跨服务后大概率丢失
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










