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

Go微服务中无法直接用数据库事务跨服务回滚
分布式系统里,单个数据库事务天然不能跨越服务边界。你写 tx.Commit() 或 tx.Rollback() 只能影响本服务的数据库连接,对其他服务里的数据毫无作用。这不是 Go 语言的限制,而是 CAP 和网络分区下的根本约束——强行要求强一致性,会牺牲可用性或引入严重延迟。
所以,别试图在 Go 微服务里“模拟”跨库 ACID 事务。真正可行的路径是:用补偿逻辑(Saga)代替回滚,用幂等 + 最终一致性兜底。
Saga 模式在 Go 中的两种落地方式
常见实现分「编排式(Choreography)」和「编排式(Orchestration)」,Go 生态更倾向后者,因为控制流清晰、错误处理集中。典型工具是 go-saga 或手写状态机,但要注意:
-
go-saga默认不带持久化,服务重启后未完成的 saga 会丢失,必须对接etcd或redis存储当前步骤和补偿指令 - 每个正向操作必须配对一个补偿函数,且补偿函数要能安全重入(比如用
UPDATE ... SET status = 'canceled' WHERE id = ? AND status IN ('pending', 'confirmed')) - 不要在补偿函数里调用新服务——它自己也得可补偿,否则链式失败不可控
Go 服务间调用时如何传递 Saga 上下文
HTTP 或 gRPC 调用中,必须显式透传 saga ID 和当前步骤序号,不能靠中间件自动注入。否则下游服务不知道自己属于哪个补偿链,也无法判断是否重复执行。
示例(gRPC):
ctx = metadata.AppendToOutgoingContext(ctx,
"saga-id", "saga_7f3a9b",
"saga-step", "2",
"saga-compensate", "true") // 补偿调用标识
下游服务收到后,应校验 saga-id 是否在本地待处理列表中,并用 saga-step 决定执行正向还是补偿逻辑。漏掉这个校验,会导致补偿被当成新请求重放。
幂等性不是可选项,而是补偿链存活的前提
网络超时、重试、补偿触发都可能导致同一操作被执行多次。Go 服务必须在入口层就拦截重复请求,常见做法:
- 用
redis.SET key value EX 3600 NX做请求指纹锁,key 是saga-id:step-name:payload-hash - 数据库唯一索引约束,比如在订单表加
external_ref_id字段并建唯一索引,避免重复创建 - 避免用时间戳或随机数做幂等键——它们无法保证跨服务一致
没做幂等的服务,一旦进入补偿流程,大概率会把数据搞乱,而不是恢复一致。
最常被忽略的点:补偿操作本身可能失败。你得有重试机制(带退避)、告警通道(比如发到 saga-failure Kafka topic),以及人工干预入口(比如提供按 saga-id 查询+手动触发补偿的 CLI 工具)。自动化只是让故障收敛更快,不是让它彻底消失。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











