saga是go微服务中唯一可落地的跨服务事务方案,因2pc、xa、tcc缺乏生产级实现或侵入性强;database/sql事务无法跨服务,dtm需手动实现幂等compensate、持久化状态及重试容错。

Saga 是 Go 微服务中唯一可落地的跨服务事务方案,其他如 2PC、XA、TCC 要么无生产级实现,要么侵入极深、运维成本爆炸。别指望 database/sql 或 gorm.Transaction 能跨服务生效——它连本地多库都管不了,更别说 HTTP/gRPC 调用。
为什么不能用 database/sql 的 tx.Commit() 跨服务
常见错误是写个 tx, _ := db.Begin(),然后串行调 http.Post("/inventory/deduct") 和 http.Post("/payment/charge"),最后 tx.Commit()。这根本不是分布式事务:tx.Commit() 只提交了本服务的数据库变更,对远程服务毫无约束力。
真实现象包括:订单创建成功、库存扣减失败、支付却已发起;日志里只看到一方超时,另一方没收到回滚指令,数据永久不一致。
-
database/sql的事务上下文完全不传播到 HTTP 请求头或 gRPC metadata 中 - Go 标准库没有事务 XID 注入机制,
context.WithValue(ctx, "xid", ...)只是标记,不是控制 - 所谓“手动两阶段提交”若没持久化状态、没重启恢复能力,就是伪协议,服务一重启就卡死在中间态
如何用 DTM 实现 Saga(避坑版)
DTM 不是银弹,它只管协调和重试,Do 和 Compensate 接口仍得你亲手写,且必须幂等、带状态校验、独立事务。
客户端初始化常 panic,根本原因是 dtmcli.NewHTTPClient 默认 1 秒超时,而 DTM server 启动慢或 Docker DNS 解析延迟。本地开发务必:
- 启动 DTM 后先
curl http://localhost:36780/health确认存活 - 显式设置超时:
dtmcli.NewHTTPClient(&dtmcli.HTTPClientOption{Timeout: 5 * time.Second}) - Docker Compose 场景下,不能只靠
depends_on,要加healthcheck+ wait-for-it.sh 或 Go 内建重试逻辑
Confirm / Cancel 接口被重复调用是常态(网络分区、server 重启都会触发 DTM 重试),每个 handler 开头必须查 DB 或 Redis 判断是否已执行:
if exists, _ := s.db.IsStepDone(ctx, gid, "deduct_stock"); exists {
return nil
}
别依赖 “DTM 只调一次”——它最多重试 10 次,指数退避。
自己手写 Saga 编排时最易踩的三个坑
没用 DTM?那更要小心:Saga 的骨架得自己搭,但多数人栽在补偿逻辑设计上。
-
Compensate不是反向操作,而是状态修正。例如退款不能写DELETE FROM payments,而应UPDATE payments SET status = 'refunded' WHERE id = ? AND status = 'charged' - 补偿必须用独立数据库连接执行,不能复用正向事务的
tx——否则tx.Rollback()会把本该保留的补偿也撤掉 - 所有补偿函数必须处理三种状态:
已执行(返回 nil)、未执行(执行并记录)、不存在(如原扣款记录已被清理,返回特定错误而非 panic)
网络超时后,SagaExecutor 不能直接认为失败——下游可能只是慢,实际已成功。正确做法是:超时后触发补偿,再主动轮询(如 GET /payments/{id}/status)确认真实状态,避免“补偿一个没发生的操作”。
状态必须落库,且字段要精简
别用 sync.Map、内存 slice 或 Redis String 存 Saga 实例状态——服务重启即丢,整个流程悬挂。必须用持久化存储,推荐 PostgreSQL 单表:
CREATE TABLE saga_instances ( id TEXT PRIMARY KEY, status VARCHAR(20) NOT NULL, -- pending / executing / compensating / finished / failed current_step TEXT, data JSONB, updated_at TIMESTAMPTZ DEFAULT NOW() );
字段越少越好,data 字段存必要上下文(如 order_id, payment_id),别塞完整 payload。每步执行前先 UPDATE ... SET status = 'executing', current_step = 'deduct_stock' WHERE id = ? AND status = 'pending',用 DB 行锁保证并发安全。
真正容易被忽略的是:补偿失败后不能静默跳过。必须写入 saga_dead_letter 表,记录 gid、step、error_message 并触发告警——这类悬挂事务无法自动恢复,得人工介入。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











