强制主库读必须显式标记,不能依赖事务自动兜底;Create后First默认走从库,需用db.Clauses(dbresolver.Write)或db.Session(&gorm.Session{Write: true})确保强一致;GORM不感知复制延迟,须业务层轮询Seconds_Behind_Master并动态更新从库列表。

强制主库读必须显式标记,不能靠事务自动兜底
事务内所有操作(包括 tx.Find())确实都走主库,但这是指“已开启的事务”,不是“刚写完就自动进入事务”。很多人误以为 Create() 后紧接着 First() 就会进事务上下文——其实不会。GORM 不会把非事务调用自动塞进隐式事务里。
常见错误是写完立刻查:db.Create(&u); db.First(&u),此时第二个 First() 默认走从库,若从库延迟 1.2s 就查不到。
- 正确做法:加
db.Clauses(dbresolver.Write)或db.Session(&gorm.Session{Write: true}) - 别用
db.Unscoped().First()试图绕过——它不改路由目标,照样走从库 - 如果封装了 DAO 方法,务必在强一致读场景的方法签名里透出
forceMaster bool参数,避免后期补救成本高
GORM 不感知 Seconds_Behind_Master,延迟判断得自己做
SHOW SLAVE STATUS 返回的 Seconds_Behind_Master 字段,GORM 连查都不会查一次。所谓“智能降级”纯属幻想。生产环境若真要基于延迟选库,必须自己维护一个从库状态 map,并定期轮询。
- 轮询频率建议 2–5 秒一次,太密加重从库压力,太疏失去意义
- 每个从库连接需单独调用
slaveDB.Raw("SELECT @@read_only").Scan(&ro)+slaveDB.Raw("SHOW SLAVE STATUS").Scan(&status) - 状态更新后必须调用
Resolver.ReplaceReplicas(),否则 GORM 仍按旧列表路由 - 注意:MySQL 8.0.22+ 的
performance_schema.replication_applier_status_by_coordinator更准,但兼容性差,老版本 fallback 到SHOW SLAVE STATUS
Raw 查询默认走从库,含写语义就直接报错
db.Raw("SELECT * FROM users WHERE id = ?").Scan(&u) 走从库没问题;但 db.Raw("SELECT COUNT(*) FROM orders FOR UPDATE") 会直接报 ERROR 1290 (HY000): The MySQL server is running with the --read-only option——因为从库 read_only=ON 是硬性要求。
- 所有带
FOR UPDATE、LOCK IN SHARE MODE的Raw必须显式加dbresolver.Write - 不要指望 GORM 解析 SQL 文本识别锁语义,它只看方法名和
Clauses - 更隐蔽的坑:
db.Raw("INSERT INTO tmp SELECT ...")在从库执行也会失败,哪怕语法上是 INSERT,GORM 仍按 Raw 默认策略发给从库
连接池与健康检查必须分开配,否则一挂全挂
主库和从库负载特征完全不同:主库连接数少、写延迟敏感;从库连接复用率高、可容忍慢查询。共用 SetMaxOpenConns(50) 或忽略 Ping(),上线三天必出问题。
- 主库连接池:设
SetMaxOpenConns(15–25)、SetConnMaxLifetime(3–5m)、SetWriteTimeout(1.5s),初始化后立即masterDB.Ping(),失败 panic - 从库连接池:每个从库实例单独调
slaveDB.SetMaxOpenConns(80)、SetConnMaxLifetime(10m),且每 30 秒执行一次slaveDB.Ping()健康探活 -
AddSlave()返回 error 必须检查,GORM 不报错也不重试,注册失败就静默退化到主库读,流量洪峰时主库直接被打满
实际最易被忽略的点是:从库 read_only=ON 必须由 DBA 提前配置好,而不是靠 GORM 自动加参数。GORM 不会在 DSN 里自动拼 ?read_only=1,如果从库没开只读,SELECT FOR UPDATE 类语句可能成功执行,但数据会脏写,后续复制冲突或主键冲突极难排查。











