Go微服务中无法用sql.Tx保证跨服务数据一致,必须采用最终一致性方案:本地事务+外发事件、Saga/TCC模式需幂等与状态持久化、补偿失败需人工干预。

Go 微服务里没法靠 sql.Tx 保证跨服务数据一致——这不是你代码写得不对,而是架构决定的。必须放弃“一次提交全部成功”的幻想,转而用可追踪、可补偿、幂等的最终一致性方案。
为什么 tx.Commit() 在微服务中完全失效
它只管得住本服务连的那个数据库连接,对其他服务的 HTTP/gRPC 调用、Kafka 消息、Redis 操作零感知。常见错误现象包括:
- 订单创建成功,但库存没扣,用户看到“已下单”却收不到货
- 补偿逻辑因消息丢失或消费者 crash 没触发,状态永久卡住
- 同一请求被重复投递,
DecreaseStock()执行两次,库存超扣
实操建议:
- 所有跨服务写操作必须拆成「本地事务 + 外发事件」两步:先落库成功,再发消息(推荐用 Kafka 事务性 producer 或本地消息表)
- 别手写 2PC 协调器——Go 生态没有生产级实现,运维成本远高于业务价值
- 每个 RPC 调用必须带
context.WithTimeout,避免单点失败拖垮整条链路
Saga 模式落地时必须持久化状态
用内存或 Redis 存 Saga 进度?节点重启就丢。状态必须落库,且带 version 字段防覆盖。
实操建议:
- 在 PostgreSQL 或 MySQL 中建
saga_log表,字段至少含:order_id、current_step、status、version、updated_at - 每步执行前先
UPDATE ... SET status='executing', version=version+1 WHERE order_id=? AND version=?,失败则重试或进死信队列 - 补偿接口如
RefundBalance()必须查 DB 确认是否已退过,不能只靠入参判断 - 不要用
goroutine直接跑补偿——没失败重试、没超时控制、没日志追踪
本地消息表比直接发 MQ 更可靠
直接调 producer.Send() 发消息,DB commit 成功但网络抖动导致消息丢失,后续步骤就永远漏掉。
实操建议:
- 建
outbox表,字段含:id、topic、payload、status('pending'/'sent'/'failed') - 关键事务内原子写入:
INSERT INTO orders和INSERT INTO outbox在同一个tx里 - 另起 goroutine 定时扫表(建议 1s 间隔 + 指数退避),调
producer.Send()并更新status - 扫表逻辑要处理重复发送:先
SELECT FOR UPDATE WHERE status='pending' LIMIT 1,再更新
TCC 模式在 go-zero 中的关键陷阱
Try 阶段不是简单校验,而是要真正预留资源;Confirm 和 Cancel 必须幂等,且不能依赖外部服务响应。
实操建议:
-
Try接口里必须完成锁库存、冻结余额等动作,并写入try_record表记录预留状态 -
Confirm只做状态确认和释放,不查第三方服务——如果支付网关不可用,它该失败就失败,由上层重试或人工介入 - 注册路由时,
/order/try、/order/confirm、/order/cancel三个路径必须独立部署,不能共用 handler - 别让
Cancel调Confirm的反向逻辑——它们是平行接口,各自保证幂等
最易被忽略的点:补偿失败不是异常情况,是必选项。无论 Saga 还是 TCC,都得有死信队列 + 人工干预通道——没有 100% 自动兜底的分布式事务。

















