go不支持内置分布式事务,需用saga模式手动编排本地事务,状态持久化、幂等性、独立补偿事务及主动状态确认是关键,消息队列+本地outbox表是推荐落地方式。

Go 里没有内置的分布式事务支持
Go 标准库和主流 ORM(如 gorm、sqlx)只管单数据库事务,tx.Commit() 或 tx.Rollback() 作用范围仅限一个 DB 连接。跨服务、跨库、跨消息队列的操作,Go 本身不提供两阶段提交(2PC)或 Saga 协调能力——这不是语法限制,而是设计取舍:Go 倾向让开发者显式控制边界,而不是封装黑盒事务。
用 Saga 模式手动编排多步操作
这是 Go 项目中最实际的落地方式:把一个逻辑事务拆成一系列本地事务,每个步骤有对应的补偿操作。关键不在“自动”,而在“可追踪 + 可重试 + 状态持久化”。
-
状态必须落库:用一张
saga_records表存trace_id、当前步骤、是否完成、重试次数,不能只靠内存或临时 map -
每步要幂等:比如扣库存接口得校验
order_status = 'pending'再更新,避免重复扣减 -
补偿动作要独立事务:回滚“创建订单”时,不是在同一个
tx里 update 订单表,而是新开连接执行UPDATE orders SET status = 'cancelled' WHERE id = ? -
别依赖 HTTP 超时判断失败:下游服务可能响应慢但最终成功,应结合主动查询(如轮询
/orders/{id}/status)确认真实状态
示例片段:
func (s *Saga) ReserveStock(ctx context.Context, orderID string) error {
_, err := s.db.ExecContext(ctx, "UPDATE inventory SET locked = locked + 1 WHERE sku = ? AND available >= 1", s.sku)
if err != nil {
return errors.New("stock reserve failed")
}
return s.recordStep(ctx, orderID, "reserve_stock", "done")
}
不要碰分布式事务中间件(如 Seata、Atomikos)
这些是 Java 生态为 JTA 设计的,Go 客户端要么缺失、要么维护停滞、要么强行套用导致架构失衡。比如用 Go 调 Seata 的 TM 接口,就得自己实现分支注册、全局锁、超时清理——相当于重写一半 Seata,还失去 Go 的并发优势。
- Seata 的
AT模式需要代理数据源并解析 SQL,Go 的database/sql没有统一 AST 层,无法安全拦截 - 若用
TCC模式,你得为每个服务暴露Try/Confirm/Cancel三个 HTTP 接口,而 Go 微服务通常只暴露 REST/GRPC 业务接口,额外加两套语义相同但行为不同的 endpoint,运维和测试成本翻倍 - 所有协调逻辑(日志存储、事务恢复、悬挂处理)都得自己补全,没现成的
seata-serverGo 版
消息队列 + 本地事务表是最可行的起点
适用于“下单成功后发通知”“支付成功后更新订单”这类最终一致性场景。核心是用数据库事务保证“业务操作 + 消息记录”原子性,再由后台 goroutine 投递。
- 建一张
outbox表,字段含payload TEXT、topic VARCHAR、status ENUM('pending','sent','failed')、created_at - 在同一个
tx里:先 insert 订单,再 insert outbox 记录,最后tx.Commit()—— 这样不会出现“订单写了但消息丢了” - 单独起一个
go outboxPoller(),定时查status = 'pending'的记录,调mq.Publish(),成功后 updatestatus = 'sent' - 投递失败时别立即重试,用
next_retry_at字段做退避,避免雪崩
注意:outbox 表必须和业务表在同一个数据库实例,否则就又回到分布式事务问题。
真正难的从来不是写几个 BeginTx 和 Rollback,而是定义清楚“哪几步必须一起成功”“哪一步失败了算整体失败”“用户看到的中间态是否可接受”。这些没法靠框架解决,得从领域模型里抠出来。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











