Go中Saga无内置支持,需手动建模Do/Undo成对步骤、持久化状态到MySQL/PostgreSQL、逆序幂等补偿,并通过DB轮询或事件驱动触发回滚。

Go 里没有“工作流引擎”或“Saga 编排器”内置支持,所谓步骤编排,本质是你用结构体 + 状态字段 + 显式调用链手动控制的补偿事务序列;回退不是自动触发的,而是靠状态表轮询 + 逆序执行幂等 Undo 函数完成的。
如何定义一个可编排的 Saga 步骤结构
不能靠函数签名或注解自动识别步骤,必须显式建模每个环节的 Do/Undo 行为和状态迁移。关键不是“怎么写逻辑”,而是“怎么让失败时能准确知道该撤哪几步”。
-
Do和Undo必须成对定义,且都接收相同业务键(如order_id、payment_id),不能只在 Do 里传参,Undo 却靠全局变量推断 - 结构体里要存
current_step(整数或枚举),而不是靠 defer 或 panic 捕获位置——panic 不会捕获 HTTP 409、数据库唯一键冲突这类“软失败” - 每步执行前,必须先更新数据库状态为
"stepN_starting",成功后再更新为"stepN_done";失败则记为"stepN_failed"并写入last_error - 示例字段:
gid(uuid)、status(pending/step1_done/compensated)、current_step(int)、payload(JSONB 存原始参数)
为什么不能用 defer 做补偿触发
defer 只在函数退出(含 panic)时执行,而 Saga 的失败绝大多数发生在非 panic 场景:gRPC 调用返回 codes.Unavailable、HTTP 返回 429、DB 执行报 sql.ErrNoRows —— 这些都不会触发 defer。
- 把
compensateDebit()写进 defer,等于假设“只要这层函数结束,前面就一定成功了”,但实际可能credit()失败了,debit()却没被补偿 - 更危险的是:defer 里的补偿函数若也失败(比如网络超时),你连日志都难定位——它不在主错误路径上
- 正确做法是:每个
Do后立刻检查 err,不为 nil 就立即调对应Undo,并确保该Undo自身幂等(比如先查支付记录状态再决定是否调退款接口)
状态持久化必须用 PostgreSQL/MySQL,不能用 Redis
Redis 作为 Saga 状态存储等于放弃事务可靠性。进程崩溃、节点重启后,内存态状态丢失,已执行的 Do 步骤无法被感知,补偿永远不会启动。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 必须建表(如
saga_instances),至少含gid(主键)、status、current_step、compensate_at、last_error - 每次推进前,用
SELECT ... FOR UPDATE加锁读取并更新状态,避免并发重复执行同一阶段 - 补偿任务不能靠内存定时器触发,得用 DB 轮询(如
WHERE status IN ('step2_failed', 'step3_failed') AND compensate_at )或事件驱动(如 Kafka topic <code>saga-failed) - Redis 可用于幂等键(如
SETNX compensation:order_123:pay 1 EX 3600),但绝不能替代状态表
空回滚和重复补偿怎么防
网络重试会让同一个 DoPayment(order_id: "O123") 被发两次;而补偿接口若没判断前置状态,就会重复退款。
- 所有
Undo函数开头必须查业务状态:比如RefundService.RollbackPayment(id)先查该订单是否已存在payment_status = 'refunded',是则直接 return - 幂等键必须带业务语义,不能只用 UUID:推荐格式
order_id:O123:step_pay,并作为数据库唯一索引或 Redis key - 不要写
if !exists { do refund }这种竞态逻辑,要用INSERT INTO compensation_log (gid, step, executed_at) ON CONFLICT DO NOTHING保证一次生效 - 补偿失败不能静默丢弃,要写入
retry_count字段,并配合指数退避重试(如 1s → 4s → 16s)
Saga 最容易被忽略的点是:它根本不是“事务框架”,而是“失败路径的显式建模”。你写的每一行 Undo,都得经得住三次重试、两次服务重启、一次数据库主从切换的考验——没落库的状态、没幂等键的补偿、没加锁的更新,任何一个缺口都会让数据卡在中间态。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










