database/sql.tx 在微服务中不适用,因其仅锁定单个数据库连接,而微服务间跨库操作无法保证原子性;saga 模式需持久化状态、保障幂等性、明确重试边界;本地消息表可解决 db 与 mq 原子写入问题;go-zero 缓存更新须严格遵循先 db 后删缓存顺序。

database/sql.Tx 在微服务里为什么不管用
因为 sql.Tx 只能锁定单个数据库连接,而微服务的每个服务通常独占自己的数据库实例(MySQL、PostgreSQL、甚至第三方 HTTP 接口)。你调用 orderSvc.Create() 和 inventorySvc.DecreaseStock() 是两个独立进程、两个独立事务,tx.Commit() 对后者完全无感知。强行包进一个 tx 里,只会让前半段成功、后半段失败,留下“订单建了但库存没扣”或“库存扣了但订单没建”的中间态。
Saga 模式落地必须做对的三件事
不是写个 if err != nil { compensate() } 就算 Saga。真正能跑通的关键在状态持久化、幂等性、重试边界:
- 所有 Saga 步骤状态(当前 step、已执行列表、补偿标记)必须落库,比如存进
saga_log表,字段含tx_id、status、version;别用内存 map 或 Redis 缓存——服务重启就丢进度 - 每个正向操作(如
deductInventory())和补偿操作(如restoreInventory())都得是幂等的:推荐用UPDATE inventory SET stock = stock - ? WHERE product_id = ? AND stock >= ?这类带条件的更新,或在 DB 加UNIQUE (order_id, action_type)索引 - 补偿失败不能静默吞掉:第 3 次重试失败后,必须写入
compensation_failed表并触发告警,而不是继续指数退避——这时候人工介入比自动重试更可靠
本地消息表比直接发 MQ 更靠谱的场景
当你发现 Kafka producer 发送偶尔失败、或者消费者查不到上下文数据(比如收到 OrderCreatedEvent 却查不到对应订单),说明你漏掉了「DB 和消息原子写入」这步。本地消息表就是为解决这个而生:
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- 消息表必须和业务表同库同
*sql.DB实例,建表用 InnoDB 引擎,status字段用TINYINT(避免字符串索引失效) - 发送逻辑必须是「扫描 + 更新 + 发送」三步在一个事务里:先
SELECT ... FOR UPDATE捞出status = 0的记录,再UPDATE message SET status = 1 WHERE id = ? AND status = 0,最后发 MQ;RowsAffected == 1才算真正抢到这条消息 - 扫描协程要用指数退避(
time.Second * 2→*4→*8),上限 30s;别用固定 1s 轮询,否则 DB 压力陡增
go-zero 里缓存一致性别碰的三个坑
cachedConn 默认走「先更新 DB,再删缓存」,这个顺序不能改,也不支持配置。但很多人踩坑在删缓存失败后的处理上:
- 别在 handler 里写
for i := 0; i —— 网络抖动会导致接口延迟飙升,甚至雪崩 - 别把
redis.Del()放进 SQL 事务里 —— 如果事务回滚,缓存删了但 DB 没改,造成「缓存空、DB 有值」的不可恢复不一致 - 别用
time.Sleep(500 * time.Millisecond)延时双删 —— 进程重启后第二次删除永远不执行,且阻塞 goroutine
真正该做的是:DB 更新成功后,投递一条 cache_delete 消息到 Kafka,由独立 job service 消费并重试;失败则记日志+告警,不卡主链路。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










