必须用saga而非本地事务的业务流程是跨服务、耗时长、涉及外部系统的流程,如下单扣库存→微信支付→发优惠券→推送物流单号→更新积分,因各服务数据库隔离且需可逆补偿。

哪些业务流程必须用 Saga 而不是本地事务
跨服务、耗时长、涉及外部系统(如支付网关、短信平台、物流接口)的流程,不能靠 database/sql.Tx 解决——因为每个服务只管自己的数据库,tx.Commit() 对下游服务完全无效。典型场景包括:下单后扣库存 → 调用微信支付 → 发优惠券 → 推送物流单号 → 更新用户积分。任意一步失败,前面已成功的操作必须可逆,且不能阻塞整个链路。
常见误判是把“事务”理解为“一次 HTTP 请求包住所有 DB 操作”。实际上,Saga 的边界由服务契约定义,不是代码块范围。比如“扣库存”必须是一个独立服务提供的幂等接口 DecreaseStock(ctx, skuID, count),而不是在订单服务里直接执行 UPDATE inventory SET qty = qty - 1。
为什么电商下单和金融转账都适合 Saga
这两类场景共同点是:步骤多、参与者异构、失败概率高、允许最终一致。下单流程中,库存服务可能临时不可用,但用户不能卡在“支付成功却没生成订单”的状态;转账中,A 账户扣款成功、B 账户入账失败,必须能触发 RefundToAccountA 补偿,而不是让资金悬空。
- 电商下单:订单创建 → 库存锁定 → 优惠券核销 → 积分扣除 → 物流单生成 —— 每步都需独立超时控制,补偿必须基于原始
order_id而非查当前余额 - 金融转账:
DeductFromAccount→AddToAccount→SendNotification—— 补偿函数不能只做“加回”,而要先查该笔转账是否已部分生效(例如 B 账户是否已到账),避免重复入账
什么情况下 Saga 反而会增加风险
短流程、强一致性要求、高频低延迟场景不适合 Saga。比如用户登录态校验、秒杀预占库存、实时风控决策——这些操作本身应在 100ms 内完成,引入补偿链路只会放大延迟和失败概率。
更危险的是把 Saga 用在“本可单库事务”的地方:例如同一 MySQL 实例下的订单表和支付表,完全可以用 tx.Exec("INSERT ...; UPDATE ...") 原子提交,硬拆成两个服务反而破坏隔离性、增加网络开销。
另一个典型坑是补偿逻辑依赖实时状态判断:比如 CancelOrder 先查“订单是否已发货”,再决定是否调用物流取消接口——这会因状态不一致导致漏补偿。正确做法是补偿操作只认 saga_id 和原始请求参数,不查当前业务状态。
dtm 中配置 Saga 的关键参数陷阱
用 dtmcli 提交 Saga 时,req.Gid、req.Steps、req.Timeout 三个字段填错会导致流程静默失败或跳过补偿:
-
req.Gid必须全局唯一且带业务上下文,例如"order_create_" + uuid.NewString();重复 Gid 会让 dtm 直接返回409 Conflict,前端收不到错误提示 -
req.Steps中每个 step 的Action和CompensateURL 必须能被 dtm 容器网络访问;K8s 环境下常因 Service 名称写成 localhost 或 Pod IP 导致卡在Waiting状态 -
req.Timeout不是单个服务超时,而是整条 Saga 链路最大容忍时间;设太短(如 2s)会导致正常耗时 3s 的支付回调被 dtm 主动中断并触发补偿,实际支付却已成功
真正难处理的不是正向执行,而是补偿失败后的悬挂状态——比如退款接口返回 context.DeadlineExceeded,dtm 重试 5 次仍失败,此时必须人工介入或依赖异步对账 Job,不能指望框架自动兜底。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











