Saga模式在Go中是多步骤本地事务加显式补偿的编排模式,需手动实现Do/Undo、逆序回滚、幂等补偿及状态持久化,无内置支持。
Go 语言本身不提供 Saga 模式内置支持,必须手动编排补偿逻辑;没有事务协调器(如 Seata)时,Saga 的可靠性完全取决于你对失败路径的覆盖程度。
什么是 Saga 模式在 Go 中的实际形态
Saga 不是某个库开个函数就能启用的机制,而是一种“多步骤、本地事务 + 显式补偿”的编排模式。在 Go 中,它通常表现为一个结构体封装一系列
和对应的
方法,外加状态机或事件驱动的执行调度逻辑。
常见误判是以为用
开启事务就能套进 Saga —— 实际上每个服务只能管自己的数据库,跨服务必须靠 HTTP/gRPC 调用 + 幂等 + 补偿,和本地 ACID 事务无关。
每个步骤必须是**本地事务性操作**(例如:PostgreSQL 中单条
+
包在
里)
每个步骤必须提供**幂等的补偿接口**(例如:
要能重复调用不报错)
正向步骤失败时,必须按**逆序执行所有已成功步骤的补偿**,且每步补偿失败也要可重试
如何用 go-zero 或纯 net/http 实现最小可行 Saga 编排
不依赖框架也能做,关键在于把“步骤注册”“顺序执行”“失败回滚”三件事拆清楚。下面是一个基于函数切片的轻量实现思路:
立即学习
“
go语言免费学习笔记(深入)
”;
注意:
是同步阻塞的,只适合单机流程编排;生产中需持久化 Saga 状态(比如写入
表),否则进程崩溃后无法恢复。
不要在
里做耗时远程调用而不设超时 —— 会导致整个 Saga 卡住
必须设计为最终一致:例如订单服务撤销时,库存服务可能还没收到通知,得靠定时任务或消息队列兜底
如果用 go-zero,可利用其
客户端的重试能力 +
存储 saga_id → step_status 映射,但需自己补全状态迁移逻辑
为什么 Saga 在 Go 微服务里容易出错
Go 的简洁性反而会掩盖分布式事务的复杂性。典型翻车点集中在三个地方:
没传到底层 HTTP/gRPC 调用,导致补偿请求永远挂起
补偿接口没做幂等判断,重复执行造成负库存或双退款
把 Saga 步骤当成普通函数调用,没记录中间状态,服务重启后无法知道“第 3 步成功了但第 4 步失败”,只能人工干预
最常被忽略的是**网络分区场景下的补偿可靠性**:比如库存服务响应超时,你认为它失败了于是执行补偿,其实它内部已扣减成功——这时补偿就会多加库存。解决办法不是避免超时,而是让库存服务的
先查当前版本号或状态再决定是否加回。
要不要用开源 Saga 库(如 Temporal、Cadence)
Temporal 的 Go SDK 确实封装了 Saga 模式(
+
配合
),但它引入了新运维面:需要部署 Temporal Server、管理历史事件存储、处理 workflow ID 冲突。
如果你的业务只有 2–3 个强一致性链路(比如下单→扣库存→发券),手写带持久化状态的 Saga 更轻量可控;一旦超过 5 步、涉及 3+ 外部系统、要求精确到秒级超时控制,Temporal 就不再是“可选”,而是必要基础设施。
别低估状态持久化的成本:哪怕只用 SQLite 存
、
、
、
四个字段,也要处理 WAL 日志刷盘、vacuum、备份恢复 —— 这些细节在压测时才会暴露。
Do()Undo()database/sqlINSERTUPDATEtx.Exec()CancelOrder(ctx, orderID)type SagaStep struct {
Do func() error
Undo func() error
}
func RunSaga(steps []SagaStep) error {
var executed []func() error
for _, s := range steps {
if err := s.Do(); err != nil {
// 逆序执行已成功步骤的 Undo
for i := len(executed) - 1; i >= 0; i-- {
executed[i]()
}
return err
}
executed = append(executed, s.Undo)
}
return nil
}
RunSagasaga_stateDo()Undo()rpcxrediscontext.WithTimeoutUndo()workflow.ExecuteChildWorkflowworkflow.ExecuteActivityworkflow.Compensatesaga_idstep_namestatusupdated_at