dbresolver.Register 必须在 gorm.Open 之后调用,否则读写分离不生效;事务中所有操作须用同一 *sql.Tx,禁止混用主从库连接;FOR UPDATE 等写语义 SQL 需显式标记 Write=true;从库延迟不自动降级,需业务层轮询并动态剔除。

dbresolver.Register 必须在 gorm.Open 之后调用
很多人把 db.Use(dbresolver.Register()) 放在 gorm.Open() 前,结果读写分离完全不生效——插件根本没绑定到任何 *gorm.DB 实例上。
正确顺序只有这一种:
- 先用
gorm.Open(dialector, &gorm.Config{})初始化一个基础*gorm.DB(此时 DSN 可填主库地址,但只是占位) - 紧接着立刻调用
db.Use(dbresolver.Register()) - 再配置
Sources和Replicas,比如dbresolver.Register(&gorm.Config{}, dbresolver.Config{Sources: []gorm.Dialector{masterDialector}, Replicas: []gorm.Dialector{slaveDialector}})
漏掉第二步,Find、First 看似走了从库,实则全是主库在扛;更糟的是,你可能误以为“已切读库”,却因没限流导致主库被打满。
事务内所有操作必须用同一个 *sql.Tx,不能混用 slaveDB.QueryRow()
最隐蔽的问题不是报错,而是数据不一致:事务里用 slaveDB.QueryRow("SELECT balance FROM users WHERE id = ?") 查余额,再用 masterDB.Exec("UPDATE users SET balance = ?") 扣款,结果基于过期快照做了错误决策。
立即学习“go语言免费学习笔记(深入)”;
原因很简单:slaveDB.QueryRow() 是另一个独立连接会话,跟事务无关,也看不到刚在 tx 里写入的数据。
正确做法只有一条路:
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
-
tx, _ := masterDB.Begin()后,所有操作必须调tx.QueryRow()、tx.Exec()等方法 - 事务中禁止出现任何对
slaveDB或dbresolver的直接调用 - 如果业务逻辑需要“事务中强一致读”,别绕弯,就用
tx实例本身
Raw("SELECT ... FOR UPDATE") 默认走从库,会直接报 ERROR 1290
dbresolver 不解析 SQL 内容,只看方法名。所以 db.Raw("SELECT * FROM orders WHERE id = ? FOR UPDATE").Scan(&o) 会被当成普通读请求,发往从库,触发 ERROR 1290 (HY000): The MySQL server is running with the --read-only option。
这类语句本质是写语义,必须强制走主库:
- 加
.Session(&gorm.Session{Write: true}):db.Session(&gorm.Session{Write: true}).Raw(...).Scan(&o) - 或用上下文标记:
ctx = context.WithValue(ctx, "gorm:write", true),再传给db.WithContext(ctx).Raw(...) - 不要依赖注释(如
/*+ write */),database/sql层根本不识别
同理,INSERT INTO ... SELECT、带 WITH ... AS 的写查询,也得显式标为写操作。
从库延迟 >5s 时 dbresolver 不会自动降级,必须业务层兜底
dbresolver 连 SHOW SLAVE STATUS 都不会执行,更别说读取 Seconds_Behind_Master。它把所有非事务 SELECT 往从库发,不管 lag 是 100ms 还是 30s。
线上出问题的典型现象:“刚支付成功,刷新订单页就显示‘未支付’”,查日志发现请求确实打到了 lag=22s 的从库。
能做的只有这些:
- 定期轮询每个从库的
Seconds_Behind_Master(用单独的健康检查 goroutine) - 维护一个可用从库列表,延迟超阈值(如 >10s)或 Ping 失败的节点,调用
resolver.ReplaceReplicas()动态剔除 - 强一致读场景(如支付结果页、账户明细页)直接用
db.Session(&gorm.Session{Write: true}),别赌从库同步速度
真正的高可用不靠框架自动切换,而靠你能及时感知延迟、快速剔除异常节点、并在关键路径上主动放弃最终一致性。

















