go中数据库一致性靠手动管理事务、锁和上下文,混用db与tx会导致部分提交;所有sql操作须统一走tx对象,select for update需配context超时与隔离级别,跨服务双写应采用saga模式并持久化补偿状态。

Go 框架本身不解决数据库一致性问题,真正起作用的是你如何用 database/sql 管理事务、锁和上下文——框架只是帮你组织代码,底层逻辑全靠你自己写对。
混用 db 和 tx 是最常踩的坑
所有 SQL 操作必须走 tx 对象,不能在事务里穿插调 db.Query 或 db.Exec。因为 db 是连接池抽象,每次调用可能拿到不同物理连接,事务只绑定在单个连接上。
- 错误现象:
tx.Commit()返回 nil,但查库发现只有部分语句生效 - 正确写法:统一用
tx.QueryRow、tx.Exec、tx.Prepare -
tx不是线程安全的,别跨 goroutine 复用;一个请求一个tx,用完即弃
SELECT FOR UPDATE 必须配超时和隔离级别
行级锁不是万能的,MySQL 默认 innodb_lock_wait_timeout=50s,而 Go 层没设 context 超时的话,协程会卡死在那里,拖垮整个连接池。
- 开启事务时必须用
db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) - context 超时建议设为 3–5 秒,比 DB 层锁等待时间短,避免“Go 放弃了,DB 还在等”
-
SELECT ... FOR UPDATE只在 InnoDB 有效,MyISAM 不支持,别在测试环境用错引擎 - 锁住的行数越多,死锁概率越高;尽量让 WHERE 条件走主键或唯一索引
跨服务双写必须放弃“原子提交”幻想
database/sql 的 tx.Commit() 对第二个数据库完全无效。两个 *sql.DB 实例之间没有事务协调能力,硬写双写失败后手动回滚第一个库,既不可靠也不幂等。
- 正确路径:先在本地库写操作日志(status=pending),再发消息(Kafka / RabbitMQ);消费端成功后,再更新日志 status=done
- 不要用
sync.Once或atomic.Value标记“已写”,进程崩溃后状态就丢了 - 每个写入步骤都要带幂等 key(如 order_id + step_name),重试时不重复扣款/扣库存
- 独立对账服务定时扫 status=pending 超过 5 分钟的记录,触发补偿或告警
Saga 模式下补偿动作必须可重入
微服务间无法用事务兜底,Saga 是目前最务实的选择,但它的可靠性不取决于流程多酷,而取决于每一步是否真的能反复执行且不翻车。
- 补偿接口内部第一件事是查“这步之前有没有做过”,比如
RefundBalance先查 refund_log 表是否存在对应 order_id 记录 - Saga 进度必须落库,字段至少含
order_id、current_step、version,更新时用WHERE version = ?防覆盖 - 别把进度存在 Redis 或内存里——节点重启就丢,Saga 流程直接断裂
- 所有 RPC 调用必须带
context.WithTimeout,超时后立即走降级或补偿,不等不扛
真正难的从来不是写 tx.Commit(),而是判断哪条语句该进事务、哪条该拆出去做成异步事件、哪条根本不能重试只能告警人工介入——这些边界得靠业务语义定,不是靠框架自动推导。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











