go标准库不支持分布式事务,需在业务层用saga模式实现最终一致性:通过状态持久化、幂等操作、异步重试及补偿动作自身可靠性保障来兜底。

Go 语言标准库不支持跨服务事务,database/sql 的 BeginTx 只能管单机数据库;HTTP/gRPC 调用失败后,上一步的 DB 提交无法回滚——这不是“要不要补偿”,而是“不补偿就数据错乱”。必须把事务控制权从数据库拿到业务层,靠状态持久化 + 幂等操作 + 异步重试来兜底。
Saga 补偿函数为什么总重复执行或漏执行
常见现象是:补偿逻辑写成 CompensateTransferOut() 直接发 HTTP 请求,没落库、没状态标记,结果网络超时返回错误,上层 panic 后重试,又跑一遍补偿,库存被加两次。
- 补偿动作本身必须先写入数据库(如
saga_records表),插入一条step = 'compensate_transfer_out'、status = 'pending'的记录 - 真实 HTTP 请求由独立 worker 轮询该表发起,成功后再
UPDATE SET status = 'compensated' - 所有
Undo函数必须查当前业务状态再操作,例如先SELECT status FROM orders WHERE id = ?,只对status = 'confirmed'的才退款 - 用数据库唯一约束防重:建表
compensation_log(order_id, step_name, created_at),INSERT 时用ON CONFLICT DO NOTHING(PostgreSQL)或INSERT IGNORE(MySQL)
如何用 select 控制 Saga 执行流,避免卡死
同步阻塞调用跨服务接口,会导致整个流程在某一步无限等待,既不能超时熔断,也无法并发推进补偿。Go 里真正可控的方式是用 select + channel 编排。
- 每个
Do()启一个 goroutine 发请求,结果写入chan error,主流程统一用select收口 - 必须同时监听
ctx.Done()和超时通道(如time.After(5 * time.Second)),一超时就触发对应Undo() - 别把
context.WithTimeout直接传给http.Client就完事——HTTP 超时只管连接/读写,Saga 层超时还要覆盖“补偿是否已执行完成” - 所有步骤的
ctx必须透传saga_id和step_id,方便日志追踪和人工干预
为什么 Outbox 模式比纯 Saga 更容易落地
很多团队一开始猛攻 Saga 状态机和补偿链路,结果发现消息丢了、下游没收到、重试没去重,最后还是退回到更稳的 outbox 模式——它把“改 DB”和“发事件”塞进同一个本地事务,天然规避双写不一致。
- 业务更新和写 outbox 表必须在同一个
*sql.Tx里:UPDATE orders SET status = 'paid' WHERE id = ?和INSERT INTO outbox (topic, payload) VALUES (?, ?) - outbox 表必须和业务表同库,否则事务无法原子提交
- 轮询 worker 不能太激进(比如 100ms 一次),建议 1–5 秒间隔,避免 DB 压力过大
- 下游服务接收事件时,必须用
idempotency-key请求头或 DB 唯一键做幂等,不能只靠内存缓存
Saga 的难点不在代码怎么写,而在每一步的 Undo 是否真能多次执行不翻车、状态是否真能被可靠扫描到、补偿请求是否真有重试闭环。很多人卡在“写了补偿函数”就以为完成了,其实漏掉了最重的一环:补偿动作自身的可靠性保障。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











