Go语言Saga需严格控制状态、幂等、补偿三要素:状态须落库并加锁读写,每步操作带唯一幂等键,补偿支持空回滚,推进逻辑串行化,补偿任务解耦执行且支持人工干预。

Go语言没有原生Saga支持,所有可用方案都依赖你对状态、幂等、补偿三要素的严格控制。它不是选个库就能跑通的事,而是围绕“事务必须可追踪、操作必须可重试、失败必须可回退”来构建一套机制。
状态必须落盘,不能靠内存或Redis
每个Saga实例(比如一次下单)需要一个持久化记录,字段至少包括:
-
gid:全局唯一ID,推荐用
uuid.NewString() -
status:枚举值如
pending、step2_done、step3_failed、compensated - current_step:当前执行到第几步(整数或字符串)
- compensate_at:触发补偿的时间戳,用于延迟/重试判断
-
last_error:完整错误信息,如
"http status 429",便于排查
每次推进前,必须用 SELECT ... FOR UPDATE 加锁读取并更新该行。进程重启、节点宕机后,靠数据库才能续上流程。把状态存在内存或Redis里,等于放弃一致性保障。
每步操作必须带幂等键,且补偿要支持空回滚
网络重试会让同一个请求打多次,正向和补偿操作都要防重复:
立即学习“go语言免费学习笔记(深入)”;
- 幂等键建议组合业务标识与步骤名,例如
order_id:O123:pay或user_id:U789:freeze,作为数据库唯一索引或 Redis key - Do 接口执行前先查该键是否存在;若已存在,直接返回成功
- Undo 接口必须先查下游真实状态——比如退款前先查支付单是否已是“已退款”,是则直接返回,不调第三方接口(即“空回滚”)
- 避免只用
order_id做幂等键,否则支付和发货共用同一 key,会互相干扰
推进逻辑只能串行,禁用 goroutine 池或 WaitGroup
Saga 是强顺序依赖的状态机,第 n 步能否执行,取决于第 n−1 步是否成功且状态已写库:
- 不要用
go doStep3()+sync.WaitGroup,这会导致补偿在正向动作完成前就触发 - 不要用 channel 阻塞等待结果,破坏状态驱动本质
- 正确方式只有两种:
– 数据库轮询:查status == "step2_done"才调doStep3()
– 事件驱动:由消息队列通知协调器,但消费端仍需加锁查 DB 状态再执行
补偿任务要解耦执行,支持人工干预
自动重试容易引发雪崩,尤其当补偿接口被限流(如支付平台退款 QPS ≤ 5)时:
- “触发补偿”和“执行补偿”必须分离:失败后立即写一条
compensation_task记录到 DB,含max_retry、retry_delay_sec(建议指数退避)、last_error - 由独立 worker 定时拉取待执行任务,而非在主流程中同步重试
- 记录必须支持人工干预:暂停、修改参数、标记为成功,这是生产环境兜底关键
- 前端反复提交导致多个 Saga 实例并发?入口处必须校验
request_id去重,或查询已有 saga 状态,避免“悬挂事务”打架


















