go中无开箱即用saga框架,本质是手动构建落库(postgresql/mysql)、幂等、串行、可补偿的状态机;须建saga_instances表持久化gid/status/current_step等字段,用select for update加锁更新,禁用内存/redis存储与goroutine并发编排。

Go 里没有现成的 Saga 协调器模块可直接 import 后 run 起来;所谓“构建”,本质是写一个状态驱动、落盘可靠、补偿可控的有限状态机,不是封装一个 SagaExecutor.Run() 就完事。
状态必须持久化到 PostgreSQL/MySQL,不能靠内存或 Redis
服务重启、节点宕机后,如果 saga_instance 行只存在内存或 Redis 中,正向步骤可能已执行,但补偿无从触发——事务就“悬挂”了。这不是 bug,是设计缺陷。
- 建表至少含:
gid(UUID)、status(如pending/step2_done/compensating)、current_step、payload(JSON)、last_error - 每次推进前,必须用
SELECT ... FOR UPDATE加锁读取并更新该行;失败则立即写入补偿任务,不重试 - 别用 Redis 存状态:它不支持行级锁,也难做原子性状态迁移;PostgreSQL 或 MySQL 才是底线选择
每个 Do/Undo 必须带幂等键,且 Undo 要支持空回滚
网络重试会让同一个请求打多次。Do 接口重复扣款、Undo 接口重复退款,都是典型后果。
- 幂等键要组合业务标识与步骤名,例如
order_id:O123:pay或user_id:U789:freeze,作为数据库唯一索引 -
Do()执行前先查该键是否存在;若已存在,直接返回成功 -
Undo()必须先查下游真实状态——比如RefundPayment(paymentID)要先查支付单是否已是“已退款”,是则直接返回,不发第三方请求 - 别只用
order_id做幂等键:支付和发货共用同一 key,会互相干扰
推进逻辑只能串行,禁用 goroutine 池、WaitGroup、chan 阻塞
Saga 是强顺序依赖的状态机。第 3 步能否执行,取决于第 2 步是否成功且状态已写库。并发编排等于主动放弃一致性。
- 错误做法:
go doStep3()+sync.WaitGroup:补偿可能在正向动作完成前就触发 - 错误做法:用
chan阻塞等待结果——破坏状态驱动本质,也掩盖超时问题 - 正确方式只有两种:
– 数据库轮询:查status == "step2_done"再调doStep3()
– 事件驱动:每步完成后发消息(如payment.charged),由独立消费者处理下一步
补偿失败必须设人工干预点,不能自动跳过
补偿本身可能失败:库存服务宕机、支付网关不可达、下游返回 409 冲突……这时候 Saga 不能停,也不能无限重试。
- 补偿失败时,把实例状态设为
failed_compensation,把完整错误(如"inventory service unreachable")存入last_error - 所有
Compensate函数必须返回明确错误,且不能忽略context.DeadlineExceeded - 对补偿失败的步骤,用指数退避重试 3–5 次,超过则标记为
failed_compensation并告警 - 关键补偿操作(如资金类)建议加「异步核对 Job」:每分钟扫一次
status = 'Compensating'的记录,调用独立的ReconcilePayment函数兜底
最常被忽略的点是:Saga 协调器本身不是“调度器”,而是“状态决策器”。它不负责发起远程调用,只根据数据库里当前 status 和 current_step 决定下一步该做什么——这个判断逻辑必须简单、确定、可测试,且不能被任何外部延迟或并发打乱。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











