Saga是Golang微服务事务的默认起点,通过拆分长流程为本地事务+补偿操作保障最终一致性,需结合发件箱模式、幂等消费与对账服务实现可靠分布式事务。

微服务中无法使用传统数据库事务保证跨服务一致性,Golang 本身也不提供分布式事务运行时——你得靠模式选型 + 工程控制,而不是找一个 BeginTx 就能解决。
Saga 模式是 Golang 微服务事务的默认起点
Saga 把一个长流程(如下单→扣库存→扣余额)拆成多个本地事务,每步成功后发事件触发下一步,失败则按反向顺序执行补偿操作。它不强求实时一致,但能保障最终正确性,且与 Go 的轻量协程、事件驱动风格天然契合。
- 用
nats.go或segmentio/kafka-go发布/订阅事件,避免服务直连;不要在 HTTP handler 里同步调用另一个服务的接口 - 每个服务消费事件后,必须在自己的数据库事务内完成状态更新,再发下一个事件——不能“先发事件再写 DB”,否则可能丢状态
- 补偿操作(如
CancelOrder、RefundPayment)必须幂等:用数据库唯一约束、UPSERT或 RedisSETNX记录已处理事件 ID - 别手写状态机调度逻辑;直接用
temporalio/temporal-go,它能自动管理 saga 流程、超时重试、补偿触发和悬挂事务恢复
消息发送必须和本地事务原子绑定
常见错误是“先写 DB,再发消息”,网络抖动或进程崩溃会导致消息丢失,下游永远收不到事件。
- 采用“发件箱模式(Outbox Pattern)”:在业务 DB 同一事务中,把事件写入一张
outbox_events表,再由独立的轮询 goroutine 异步读取并投递到消息队列 - 轮询器需带
FOR UPDATE SKIP LOCKED(PostgreSQL)或乐观锁(MySQL),防止多实例重复投递 - 投递失败的消息要进死信队列(DLQ),不能静默丢弃;建议用 NATS JetStream 或 Kafka 的 compact topic 存储待重试事件
TCC 仅在金融级场景谨慎启用
TCC(Try-Confirm-Cancel)能提供比 Saga 更强的一致性,但开发成本高、易出错,95% 的电商、内容类业务完全不需要。
立即学习“go语言免费学习笔记(深入)”;
-
Try阶段必须写入状态表(如account_frozen_balance),标记资源预留,且该记录要带expire_at -
Confirm和Cancel必须设计为可重入:同一事务 ID 多次调用不能引发数据错乱;推荐用UPDATE ... WHERE id = ? AND status = 'frozen'做条件更新 - 必须配定时任务扫描“悬挂事务”(
status = 'frozen'但超过expire_at);Go 中可用github.com/robfig/cron/v3每分钟扫一次 - 别把 TCC 逻辑混进业务 handler;用 middleware 或 decorator 封装,通过
context.WithValue透传transaction_id
消费者端必须实现幂等与至少一次语义
消息中间件只能保证“至少一次投递”,网络分区、超时重试、服务重启都会导致重复事件。你的业务代码必须自己扛住。
- 每个事件结构体必须含全局唯一
event_id字段(如 UUID v4),且在消费前查库确认是否已处理 - 避免用“查订单状态 → 扣库存”这种两阶段判断;改用
UPDATE inventory SET stock = stock - 1 WHERE product_id = ? AND stock >= 1,靠 DB 返回影响行数判断是否成功 - 对账服务不是可选项——每天跑一次 SQL 对比订单、库存、账户三张表的净变动,发现不一致就告警人工介入;它兜底所有异步链路的不可靠性
真正难的从来不是写 tx.Commit(),而是定义清楚每个服务的数据主权边界、事件语义粒度、以及失败时谁负责兜底。Saga + 发件箱 + 幂等消费 + 对账,这套组合拳在 Go 生态里已经足够成熟,别被“分布式事务”这个词吓住——它只是把原来单体里藏在框架里的复杂性,显式地搬到你的代码里而已。










