Saga模式是Go微服务中实现最终一致性的务实选择,它将业务流程拆分为多个幂等、可补偿、状态持久化的本地事务,通过异步消息驱动与重试机制保障不丢、不错、不重。

Go 微服务里没法用 tx.Commit() 跨服务保证一致性——这不是你代码写得不对,是分布式系统本身就不支持。 必须放弃“一次全部成功”的幻想,转而用可追踪、可补偿、可重放的机制来兜住数据状态。
为什么 database/sql 的事务在微服务里直接失效
单个服务调用 sql.Tx 只能锁住本机一个数据库连接。订单服务写 MySQL、库存服务写 PostgreSQL、支付服务调用第三方 HTTP 接口——这三个操作之间没有共享事务上下文,tx.Commit() 对其他服务完全无感知。强行封装“伪 2PC”会导致:网络分区时事务悬挂、超时后状态不可知、运维排查成本爆炸。
常见错误现象:
- 订单已创建,但库存没扣,用户看到“已下单”却一直不发货
- 消息发出去了,但消费者 crash 后没重试,补偿逻辑压根没触发
- 同一请求被 Kafka 重复投递两次,库存被扣两次(没做幂等校验)
用 Saga 模式实现可落地的最终一致性
Saga 是 Go 微服务中最务实的选择:它把一个业务流程拆成多个本地事务,每步都落库、每步都有补偿、每步都可查可重放。关键不在“多酷”,而在“不丢、不错、不重”。
实操建议:
- 所有中间状态必须持久化到 DB(比如
transaction_log表),字段至少包含:trace_id、current_step、status、created_at、updated_at;别依赖内存或临时channel,服务重启就丢进度 - 每个正向操作(如
deductInventory())和补偿操作(如restoreInventory())必须幂等;推荐在 DB 加唯一索引(如UNIQUE(order_id, step_name))或用WHERE status = 'pending'做条件更新 - 用
context.WithTimeout显式控制每个 RPC 调用超时,避免某一步卡死导致整个 Saga 流程阻塞 - 不要手写协调器——用
temporal.io或asynq这类带状态持久化和重试能力的工具接管执行调度
消息可靠性 + 幂等消费是底线保障
哪怕用了 Saga,消息层仍是单点风险。Kafka 或 RabbitMQ 不保证“只投递一次”,只保证“至少一次”。所以消息发送和消费两端都得加固。
实操建议:
- 业务本地事务提交成功后,再发消息(即“先 DB 后 MQ”);若发消息失败,靠定时任务扫描未发状态的记录并重试
- 消费者端必须做幂等:用 Redis
SETNX记录已处理的message_id,或在 DB 插入带唯一约束的消费记录表 - 避免在 Gin HTTP handler 里直接调用补偿逻辑;用异步队列(如
asynq)解耦,防止主链路被补偿任务拖慢甚至雪崩 - 给每个补偿任务设
max_retry = 3和指数退避(如time.Second * 1、* 2、* 4),失败日志打到 Loki,并监控compensation_failed_count指标
真正容易被忽略的不是“怎么选模式”,而是状态持久化的粒度和补偿边界是否清晰。Saga 每步失败后该回滚到哪?TCC 的 Try 阶段资源冻结是否可逆?消息重试时要不要跳过已成功步骤?这些细节不落在代码和表结构里,光靠文档或口头约定,迟早出事。

















