Go微服务中无法实现强一致,Saga+本地消息表是最可控方案;database/sql.Tx因跨服务失效;本地消息表须与业务表同库以保证原子性;补偿需异步幂等且带重试与告警。

Go 微服务里没有“强一致”的银弹,最终一致性不是妥协,而是对分布式系统物理约束的诚实回应。Saga 模式 + 本地消息表是当前最可控、最易落地的组合,其他方案要么不可用(2PC)、要么强耦合业务且难兜底(TCC)。
为什么 database/sql.Tx 在微服务里直接失效
因为 Begin 和 Commit 只作用于单个 *sql.DB 连接,而微服务天然跨进程、跨数据库实例。你调用用户服务扣余额,再调用订单服务建单,这两个操作不在同一个事务上下文里——tx.Commit() 根本管不到远程 HTTP/gRPC 调用。
常见错误现象:
- 余额已扣,订单没创建成功,用户钱没了但没下单
- 订单创建成功,库存服务超时未响应,库存数没变但订单状态已是 “已支付”
这不是代码 bug,是架构层面的必然结果。别试图用 defer tx.Rollback() 包住所有远程调用,它什么也拦不住。
本地消息表必须和业务表在同一个数据库
这是保证「业务写入 + 消息落库」原子性的唯一方式。如果消息表在另一个库,哪怕只差几毫秒,就可能出现订单写成功但消息丢失,下游永远收不到事件。
实操要点:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- go-zero 中必须共用同一个
*sql.DB实例,不能各自sql.Open() - 建表语句禁用
ENGINE=MyISAM,否则SELECT ... FOR UPDATE失效 -
status字段用TINYINT(0=待发送,1=已发送),避免字符串索引失效 - 插入消息必须和业务更新在同一个
tx.Commit()里完成
补偿任务必须异步化且带幂等控制
HTTP handler 里直接调 compensateInventory() 是危险操作:网络抖动、下游超时、panic 都会导致主链路 hang 死,goroutine 积压,QPS 断崖下跌。
正确做法是把补偿推到队列:
- 用
asynq.Client或entgo+ cron job 做持久化调度,别用time.AfterFunc - 补偿任务设
max_retry = 3,重试间隔用指数退避(1s → 2s → 4s) - 幂等不靠内存 map 或临时标记,而靠 DB 唯一约束:
UNIQUE (order_id, action),或条件更新:UPDATE inventory SET stock = stock + ? WHERE sku_id = ? AND status = 'frozen'
状态必须持久化,不能靠内存或 context 传递
Saga 执行中途服务重启,靠 defer 或 context.WithCancel 是留不住状态的。每一步执行前要落库记录「即将执行 X」,成功后立即更新为「X 已完成」,失败时查状态表逆序触发对应补偿。
最容易被忽略的点:
- 补偿函数本身也要幂等——恢复库存不能无脑加回 N 件,得先查
inventory_compensation_log是否已存在该order_id - 第 3 次重试失败后,必须写入
compensation_failed表并触发告警,人工介入比盲目重试更可靠 - 所有 RPC 调用都要包
context.WithTimeout(ctx, 3*time.Second),防止某一步卡死拖垮整个 Saga 流程
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










