Go原生不支持跨服务事务,Gin仅是HTTP路由框架,无法协调分布式事务;Saga模式需自主实现SagaExecutor、Do/Compensate接口对及持久化状态存储,补偿操作必须幂等、带前置校验且轻量。

Go 原生不支持跨服务事务,Gin 只是 HTTP 路由层
Gin 本身完全不参与事务协调——它只是个 Web 框架,gin.Context 里塞 context.WithValue 传个 tx 或 xid,对下游服务毫无作用。常见错误是写完 db.Begin() 就串行调 http.Post("inventory/deduct"),结果订单入库成功、库存扣减超时失败,defer tx.Rollback() 根本 rollback 不了远端。
真正的问题不在 Gin,而在你是否混淆了「本地事务边界」和「服务调用边界」。Gin handler 里的 tx.Commit() 只管当前 DB 连接;HTTP 请求发出去后,就脱离了这个事务上下文。
Saga 模式必须自己实现三要素,没有 gin-saga 插件
别指望 go get gin-saga 这类不存在的包。一个可运行的 Saga 必须包含:
-
SagaExecutor:编排步骤顺序,失败时反向调补偿(不能靠 goroutine + channel 模拟,必须落库记录状态) -
Do/Compensate接口对:每个服务动作(如ChargePayment)必须配独立、幂等的补偿(如RefundPayment),且路径不能复用同一 endpoint 加 query 参数区分 - 持久化状态存储:用 PostgreSQL 表或 Redis Hash 存
saga_id、current_step、status(pending/compensating/finished)、payload;内存 map 或无持久化的 Redis key 重启即丢,Saga 直接卡死
补偿函数不是“逆操作”,而是带前置校验的状态修正
典型错误:CompensateCreateOrder() 直接 DELETE FROM orders。这会破坏外键约束、引发下游服务 panic。
立即学习“go语言免费学习笔记(深入)”;
正确做法是原子更新状态:
UPDATE orders SET status = 'canceled' WHERE id = ? AND status = 'created'
关键点:
- 补偿必须基于原始请求 ID(如
payment_id),而非查当前余额再退——竞态下查到的是中间态 - 所有
Compensate函数开头先查是否已执行:SELECT refunded FROM payments WHERE id = ?,已存在则直接 return nil - 网络失败导致重试时,
RefundPayment()必须能处理三种响应:already_refunded、payment_not_found、refunding_in_progress,不能 panic - 补偿逻辑应尽量轻量:一条 SQL 或一次带 3s 超时的 HTTP 请求,别在补偿里做远程查询或复杂计算
DTM/Dapr 只省协调器,Do/Undo 还得你写
DTM 提供 /api/dtmsvr/newTransaction 和 /api/dtmsvr/submit,但它不帮你实现 /inventory/deduct 的幂等扣减逻辑,也不生成 /inventory/release 的补偿接口。Dapr 的 transactionalStateStore 同样只管状态提交,不定义业务语义。
你仍需手动确保:
- 每个子事务接口支持空重试:
UPDATE stock SET frozen = frozen + ? WHERE sku_id = ? AND frozen (用条件更新代替先查后改) - 所有 Do 接口返回明确错误,所有 Compensate 接口返回明确错误,且
context.DeadlineExceeded不被忽略 - 关键资金类补偿加异步核对 Job:每分钟扫
status = 'Compensating'记录,调ReconcilePayment兜底
最易被忽略的其实是状态持久化的可靠性——Saga 卡住不是因为逻辑写错,而是因为 saga_instances 表没加唯一索引、没设 ON CONFLICT DO NOTHING,导致并发写入覆盖或重复插入,后续状态无法收敛。


















