不注册dbresolver插件则读写分离完全失效;必须在gorm.Open()后立即调用db.Use(dbresolver.Register()),主从库需各自独立sql.Open并差异化调优连接池参数,路由依据方法语义而非SQL解析,强一致读须显式标注Write。

不注册 dbresolver 插件,读写分离就不存在;主从共用一个 *sql.DB 实例,连接池参数会互相污染、故障时直接拖垮整条链路。
db.Use(dbresolver.Register()) 必须在 gorm.Open() 之后立即调用
很多人把 db.Use(dbresolver.Register()) 放在 gorm.Open() 前,结果插件根本没绑定成功。GORM v2 要求插件必须作用于已初始化的 *gorm.DB 实例,否则所有 Find、First 都静默 fallback 到主库,日志里不报错也不提示。
正确顺序只有这一种:
- 先
db, err := gorm.Open(mysql.Open(dsnMaster), &gorm.Config{})—— 这里的dsnMaster只是占位,不参与实际路由 - 紧接着立刻
db.Use(dbresolver.Register(...)),传入含Sources和Replicas的完整配置 - 漏掉第二步,整个配置就是“假分离”,压测时才发现读请求全打主库
主库和每个从库必须各自 sql.Open,不能复用或浅拷贝
主库和从库是负载特征完全不同的服务端点:主库扛写、事务、强一致读;从库只处理最终一致性查询。共用一个 *sql.DB 或复用配置指针,会导致连接池参数错乱、健康状态无法隔离、故障时互相拖累。
实操要点:
- 主库单独
sql.Open,建议:SetMaxOpenConns(50)、SetWriteTimeout(3 * time.Second)、SetConnMaxLifetime(5 * time.Minute) - 每个从库也单独
sql.Open,不能复用主库对象,建议:SetMaxOpenConns(100)、SetReadTimeout(10 * time.Second)、SetConnMaxLifetime(10 * time.Minute) - 注册前务必对每个从库调用
slaveDB.Ping(),失败要记录告警——AddSlave或Replicas中地址不可达时不会报错,但后续读请求会静默 fallback 到主库
路由靠方法语义,不是 SQL 解析,Raw 查询最容易踩坑
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())强制走主库,没法中途切到从库
强一致读(如支付前查余额)必须显式标注:db.Clauses(dbresolver.Write).First(&u),否则可能读到旧数据。
延迟与节点故障得靠业务层兜底,GORM 不做自动剔除
dbresolver 是静态配置,注册完就不管了。它不感知复制延迟,也不查 SHOW SLAVE STATUS,更不会自动剔除宕机节点。
这意味着:
- 从库
Seconds_Behind_Master = 12时,Find仍照常转发,导致“刚提交就查不到” - 要实现延迟感知,得额外维护从库状态
map,定期轮询各节点并调用Resolver.ReplaceReplicas()动态刷新 - 事务内所有操作绑定主库连接,不能靠“从库快就切过去”——事务一致性优先级高于负载分担
最常被忽略的是:没有主动检查从库连通性 + 没有为强一致场景加 Clauses(dbresolver.Write),这两点一漏,线上就出数据不一致问题。


















