go微服务中无开箱即用saga框架,需基于context、状态机、消息队列和幂等/可重试补偿逻辑手动构建,核心是正向与反向操作的可靠编排。

Go 微服务里没有“分布式事务”这回事,只有可控的最终一致性。硬套 ACID 或强一致模型,90% 的项目会卡在超时、悬挂、补偿失败上。
本地消息表为什么必须和业务表同库
因为 INSERT INTO orders 和 INSERT INTO messages 必须在一个 tx.Commit() 里完成,否则就不是原子操作。跨库写入(比如订单写 MySQL、消息写 Redis)无法保证“要么都成功,要么都失败”。
- 消息表字段至少包含:
id、biz_type、payload、status(pending/sent/failed)、created_at、next_retry_at - 插入顺序不能颠倒:先写业务数据,再写消息记录,且必须用同一
*sql.Tx - 投递任务扫描时要用
SELECT ... FOR UPDATE SKIP LOCKED,避免多个 worker 重复处理同一条消息 - 失败重试要指数退避,比如首次 1s,再失败则 2s、4s、8s,上限设为 5 分钟
Saga 补偿函数不能只传 ID
很多团队把补偿写成 CompensateOrderCancel(orderID),结果订单早已被用户取消、库存已被其他单占用、商品已下架——直接恢复库存会超卖。
- 补偿函数签名应带原始请求快照:
func(ctx context.Context, req *CreateOrderRequest) error - 执行前必须查当前状态:
SELECT status, stock_version FROM inventory WHERE sku = ?,比对是否仍可释放 - 更新库存必须带乐观锁:
UPDATE inventory SET stock = stock + ?, version = version + 1 WHERE sku = ? AND version = ? - 补偿失败不能静默丢弃,要写入
compensation_failures表,留人工干预入口
DTM 和 Seata-Go 的启动失败常见原因
两者都不是“引入就跑”,配置错一个字段就能让整个 Saga 卡住不动。
-
dtm启动失败常见于dtmcli.SagaNew调用时返回context deadline exceeded:检查 dtm-server 是否监听在正确地址,以及客户端是否用了WithTimeout但设得太短(建议 ≥15s) -
seata-go报no available service:本质是registry.type(如nacos)连不上,或service.vgroupMapping中的分组名(如my_test_tx_group)和服务端不一致 - 两者都要求所有参与服务提供幂等接口,否则
Confirm或Try重试会写脏数据;建议每个接口开头加SELECT FOR UPDATE锁住业务主键再判断是否已处理 - HTTP 中间件注入
XID头时,Gin 的中间件顺序必须在路由匹配之后、handler 执行之前,否则tx.Begin()拿不到上下文
最易被忽略的一点:Saga 进度状态不能存在内存或 Redis,必须落库且带 version 字段。节点重启后,靠查询数据库才能续跑;没这个,整个流程就断在半路,没人知道该补哪一步。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











