跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

Golang怎么做分布式事务_Golang分布式事务教程【完整】

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-server
Go 版 消息队列 + 本地事务表是最可行的起点 适用于“下单成功后发通知”“支付成功后更新订单”这类最终一致性场景。核心是用数据库事务保证“业务操作 + 消息记录”原子性,再由后台 goroutine 投递。 立即学习 “ go语言免费学习笔记(深入) ”; 建一张
outbox
表,字段含
payload TEXT
、
topic VARCHAR
、
status ENUM('pending','sent','failed')
、
created_at
在同一个
tx
里:先 insert 订单,再 insert outbox 记录,最后
tx.Commit()
—— 这样不会出现“订单写了但消息丢了” 单独起一个
go outboxPoller()
,定时查
status = 'pending'
的记录,调
mq.Publish()
,成功后 update
status = 'sent'
投递失败时别立即重试,用
next_retry_at
字段做退避,避免雪崩 注意:
outbox
表必须和业务表在同一个数据库实例,否则就又回到分布式事务问题。 真正难的从来不是写几个
BeginTx
和
Rollback
,而是定义清楚“哪几步必须一起成功”“哪一步失败了算整体失败”“用户看到的中间态是否可接受”。这些没法靠框架解决,得从领域模型里抠出来。

相关文章