go语言saga模式需严格依赖状态持久化、幂等控制与补偿可重入:状态必须存mysql/postgresql,每步加锁更新;do/undo须带唯一幂等键并处理空回滚;推进仅支持db轮询或事件驱动,禁用并发编排与同步阻塞。

Go 语言没有 Saga 模式原生支持,也不存在“开箱即用”的可靠实现——所有能跑通的 Saga,都建立在你对状态落盘、幂等控制和补偿可重入的严格把控上。
Saga 状态必须持久化到数据库,不能放内存或 Redis
内存或 Redis 存 Saga 状态(比如 saga_instance 表里的 status、current_step、payload)等于放弃一致性。进程重启、节点宕机后,正向步骤已执行但补偿无从触发,事务就“悬挂”了。
必须用 PostgreSQL 或 MySQL 建表,字段至少包含:
-
gid(全局事务 ID,建议用uuid.NewString()生成) -
status(枚举值如pending/step1_done/step2_failed/compensated) -
current_step(整数或字符串,标识当前执行到哪步) -
compensate_at(时间戳,用于判断是否该触发补偿) -
last_error(文本字段,存完整错误如"http status 429")
每次推进前,用 SELECT ... FOR UPDATE 加锁读取并更新状态,失败则 rollback 并立即写入补偿任务。
每个 Do/Undo 接口必须自带幂等键,且补偿要支持“空回滚”
网络重试会让同一个 DoPayment(order_id: "O123", amount: 100) 被调两次。如果没幂等控制,就会扣两次款。
正确做法是:所有正向操作和补偿操作都带业务唯一键,例如 order_id:O123:pay,并作为数据库唯一索引或 Redis SETNX compensation:order_123:pay 1 EX 3600 的 key。
补偿接口还必须能处理“原 Do 根本没执行”的情况(即空回滚),比如 RefundService.RollbackPayment(id) 要先查支付记录状态,已是“已退款”就直接返回,不调第三方接口。
常见错误包括:
- 把
Do和Undo写在同一 HTTP handler 里靠 query 参数区分——DTM 或自研协调器无法识别语义,会跳过补偿 - Undo 接口直接发退款请求,不查下游当前状态,导致重复退款或报错中断
- 幂等键只用
order_id,没带上 step 名称,造成跨步骤冲突(如pay和ship共享同一 key)
Saga 必须串行驱动,禁用 goroutine 池、WaitGroup、chan 阻塞编排
Saga 不是并发任务流,而是强依赖的线性状态机。第 3 步能否执行,取决于第 2 步是否成功且状态已写库。用 go doStep3() + sync.WaitGroup 或 chan 等结果,会彻底破坏顺序性——补偿可能在正向步骤完成前就触发。
正确推进方式只有两种:
- 数据库状态轮询 + 条件分支:
if saga.Status == "step2_done" { doStep3() },每步都查 DB 状态再决定动作 - 事件驱动(推荐):每步成功后发消息(如 Kafka event),由独立消费者监听
saga.step2.done事件再触发doStep3()
绝对不要做这些:
- 把所有
Do扔进go-co-op/workerpool—— 并发乱序无法收敛 - 用
context.WithTimeout包裹整个 Saga 流程——超时只中断当前 goroutine,不触发补偿,状态卡死 - 用
http.DefaultClient.Do同步阻塞等待每个 HTTP 调用——一个慢接口拖垮全部流程,且无法熔断重试
补偿必须异步解耦,且支持人工干预
硬编码 for i := 0; i 是生产环境雷区。支付平台退款接口常限流(QPS ≤ 5),硬重试只会放大雪崩。
正确做法是把“触发补偿”和“执行补偿”分离:
- 失败时,仅往
compensation_task表插入一条记录,含字段:gid、step_name、max_retry(默认 5)、retry_delay_sec(用指数退避,如 1, 2, 4, 8, 16)、last_error - 由独立 worker(如 cron job 或 long-polling service)定时拉取待执行任务,按
retry_delay_sec控制节奏 - 提供管理界面或 CLI,支持人工暂停 / 修改重试次数 / 标记为成功(比如确认退款已到账)
最容易被忽略的点是“悬挂事务”兜底:没有定期扫描 status = 'pending' 且 updated_at 的记录并告警或自动补偿,系统就永远不知道哪些 Saga 卡住了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











