读写分离必须先调用gorm.Open再立即执行db.Use(dbresolver.Register()),否则所有读请求静默走主库;主从库需独立sql.Open并分别调优连接池;路由依据方法语义而非SQL内容,事务内强制走主库。

不调用 db.Use(dbresolver.Register()),读写分离就根本不存在——所有读请求都会静默 fallback 到主库,压测时才暴露问题。
dbresolver.Register 必须在 gorm.Open 之后立即调用
很多人把 db.Use(dbresolver.Register()) 放在 gorm.Open() 前,结果插件完全没注册上。GORM v2 要求插件必须绑定到已初始化的 *gorm.DB 实例上,否则 Find、First 等方法全部走主库,日志里既不报错也不提示。
正确顺序只有这一种:
- 先执行
db, err := gorm.Open(mysql.Open(dsnMaster), &gorm.Config{}) - 紧接着立刻执行
db.Use(dbresolver.Register())
注意:dsnMaster 在 gorm.Open() 里只是占位作用,实际路由由 dbresolver.Config 中的 Sources 和 Replicas 决定;漏掉 .Use() 这一步,整个配置就是“假分离”。
每个主库和从库必须独立 sql.Open 并调优连接池
主库和从库是负载特征完全不同的服务端点,不能复用同一个 *sql.DB 或浅拷贝配置指针,否则连接池参数错乱、健康状态无法隔离、故障时互相拖累。
必须为每个库单独调用 sql.Open,并分别设置连接池参数:
- 主库建议:
SetMaxOpenConns(50)、SetWriteTimeout(3 * time.Second)、SetConnMaxLifetime(5 * time.Minute) - 每个从库单独配置,建议:
SetMaxOpenConns(100)、SetReadTimeout(10 * time.Second)、SetConnMaxLifetime(10 * time.Minute) - 注册前务必对每个从库调用
slaveDB.Ping(),失败要记录告警——AddSlave或Replicas中地址不可达时不会报错,但后续读请求会静默 fallback 到主库
路由靠方法语义,不是 SQL 解析
GORM 不解析你写的 "SELECT ... FOR UPDATE" 或 "INSERT ... SELECT",它只看调用方式。默认规则简单粗暴,但容易踩坑:
-
Create/Save/Update/Delete/Begin→ 强制走Sources(主库) -
Find/First/Where().Scan()/Model().Count()→ 默认走Replicas(从库) -
Raw("SELECT * FROM users").Scan()默认走从库;但Raw("INSERT INTO ...")或Raw("SELECT ... FOR UPDATE")会直接在从库报错ERROR 1290 (HY000): The MySQL server is running with the --read-only option - 事务内所有操作(包括
tx.First())强制走主库,哪怕只是查刚写入的数据 - 强一致读(比如刚
Create完立刻First)必须手动切主库:db.Session(&gorm.Session{Write: true}).First(&u)或db.Clauses(dbresolver.Write).First(&u)
从库延迟需业务层自行兜底
GORM 本身不感知 Seconds_Behind_Master,也不会因为从库慢就自动切回主库读。它连查 SHOW SLAVE STATUS 的能力都没有。
不要依赖 “GORM 自动处理延迟”。需要延迟感知负载均衡,得额外维护一个从库状态列表,定期轮询各节点 Seconds_Behind_Master,并在选库前过滤掉延迟 >10s 或不可达的实例。
最常被忽略的一点:事务内所有读操作都绑死在开启事务的那个主库连接上,dbresolver.Read 在事务中完全无效——这个限制在文档里藏得很深,但线上出过多次数据不一致事故。











