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