go中需手动实现saga分布式事务,必须显式设计正向/补偿逻辑、持久化状态、保证幂等与可观测性,不可依赖内存或同步http调用。

Go 里没有现成的 Saga 框架,得自己搭骨架
别指望 go get 一个包就搞定分布式事务。Go 生态里没有像 Java 的 Seata 或 .NET 的 MassTransit 那样开箱即用的 Saga 实现。gin、echo、gRPC 这些只管通信,不负责跨服务状态编排。你得自己定义补偿函数、设计状态流转、持久化中间状态——这不是调个 Saga.Execute() 就完事的。
常见错误是把所有步骤塞进一个 HTTP handler 里串行调用:createOrder() → deductInventory() → chargePayment(),失败时只 rollback 本地 DB。结果支付成功了,库存没扣,订单却创建失败,没人触发退款。
- 必须显式拆分正向和补偿逻辑,比如
ChargePayment()和RefundPayment(),二者参数结构尽量对称 - 补偿不能写在业务 handler 里;它要能被独立触发(比如通过消息或定时任务),且可单独测试
- goroutine + channel 不适合做 Saga 编排:无法取消、无超时语义、不带重试,纯属误用
状态必须落库,别存在内存或 Redis 里
所有中间状态必须持久化到磁盘,否则服务重启就丢进度。用一张 saga_instances 表存 id、current_step、status(如 pending/compensating/finished)、data(JSON 字段存上下文),字段至少含这四个。
常见坑是用 map[string]*SagaInstance 存内存,或者往 Redis 写个 TTL 为 1 小时的 key —— 节点宕机或滚动更新后,Saga 实例直接消失,用户看到“下单成功”,但库存没扣、积分没加,系统进入不可恢复的脏状态。
- 每步执行前先查
saga_instances.status:如果是compensating,跳过正向逻辑,直奔补偿分支 - 不要依赖临时
channel或 context.Value 传事务上下文;所有关键信息必须落库 - 状态更新和业务操作要放在同一个本地事务里(比如用
entgo的Transaction包裹),避免状态更新成功但业务失败
每个正向操作和补偿操作都必须幂等
网络不可靠,重试是常态。一次 deductInventory() 可能因超时被重发三次,结果库存被扣三次;一次 restoreInventory() 若没判断当前库存是否已恢复,可能把库存加回三次。
实现幂等最简单可靠的方式:DB 加唯一索引 + 条件更新。比如扣库存时,SQL 写成 UPDATE inventory SET total = total - 1 WHERE sku_id = ? AND status = 'available';补偿时,用 WHERE status = 'frozen' 做前提。
-
Do接口不能依赖前序步骤成功(因为可能重试),比如扣库存不能查“订单是否已创建”,而应查“该订单的库存预留记录是否存在” -
Undo必须支持空回滚:原Do根本没执行,Undo也要能安全返回成功,不能报错 - 所有接口返回必须含明确的
result字段(如{"result": "success"}),别只靠 HTTP 状态码或 error != nil 判断
别用同步 HTTP 调用串起 Saga 步骤
用 http.Post 同步等响应,第一步耗时 2s,第二步卡住 30s,整个 Saga 实例 hang 死,下游还在不断重发请求,形成雪崩。
正确做法是拆开执行流:每步走异步消息(如 RabbitMQ/Kafka),由独立消费者处理,并配 per-step 的 timeout 和 retry 策略。Go 里推荐用 streadway/amqp + context.WithTimeout() 包裹每个远程调用,统一处理 context.DeadlineExceeded。
- 超时后不立即失败,先更新数据库为
timeout状态,再触发补偿——避免“超时但实际成功了”的双花问题 - 补偿失败时,别无限重试;设
max_retry = 3,失败后置状态为failed_compensation,把错误原因(如"inventory service unreachable")写进error_message字段,同时发告警 - DTM 等框架要求
Action和CompensateURL 必须是绝对路径(如http://inventory-svc:8080/v1/sku/deduct),相对路径会直接请求 localhost
Saga 的复杂点不在代码量,而在状态决策边界和失败后的可观测性。最容易被忽略的是:补偿本身可能失败,而你得让这个失败可定位、可告警、可人工介入——不是写完 Compensate() 就算完事。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











