gorm读写分离必须显式注册dbresolver插件、严格分离主从*sql.db实例、按方法语义路由,否则读走主库或写入从库报错;dbresolver.register须在gorm.open后调用,主库设writetimeout,从库需单独ping校验,raw查询不解析sql语义,强一致读须显式标注write策略,延迟与故障须业务层兜底。

GORM 的读写分离不是配个参数就自动生效,必须显式注册 dbresolver 插件、严格分离主从连接池、手动对齐路由意图,否则所有读请求仍走主库,或写操作误发到从库报 ERROR: cannot execute INSERT in a read-only transaction。
dbresolver.Register 必须在 gorm.Open 之后调用
很多人把 db.Use(dbresolver.Register()) 放在 gorm.Open 前,结果插件根本没绑定成功——dbresolver.Register 只能作用于已初始化的 *gorm.DB 实例。
正确顺序是:
- 先用
gorm.Open(mysql.Open(masterDSN), &gorm.Config{})初始化基础*gorm.DB(此时 DSN 仅作占位,不承担实际路由) - 紧接着调用
db.Use(dbresolver.Register(...)),传入含Sources(主库)和Replicas(从库列表)的配置 - 漏掉第二步,
Find、First等方法全部走主库,“分离”只是假象
主库和从库必须是两个独立的 *sql.DB 实例
共用一个 *sql.DB 或复用配置指针,会导致连接池参数错乱、健康状态无法隔离、故障互相拖累。
实操要点:
- 主库:单独
sql.Open,设SetMaxOpenConns(20)、SetWriteTimeout(3 * time.Second)、SetConnMaxLifetime(5 * time.Minute),初始化后立刻db.Ping(),失败直接panic - 每个从库:也必须单独
sql.Open,不能复用主库对象;设SetMaxOpenConns(100)、SetReadTimeout(10 * time.Second)、SetConnMaxLifetime(10 * time.Minute) -
db.AddSlave()返回 error 时必须检查并告警,否则从库不可达会静默 fallback 到主库,日志无提示
Raw 查询默认走从库,但 SQL 语义不被解析
GORM 不解析 "SELECT ... FOR UPDATE" 或 "INSERT INTO ... SELECT" 这类 SQL 文本,只按方法名硬路由。这意味着:
-
db.Raw("SELECT * FROM users WHERE id = ? FOR UPDATE").Scan()默认发到从库,直接触发ERROR 1290 (HY000): The MySQL server is running with the --read-only option -
db.Raw("INSERT INTO logs SELECT * FROM tmp_logs").Exec()同样会被当“读操作”发到从库,报错 - 带锁查询(
FOR UPDATE、LOCK IN SHARE MODE)必须显式加db.Session(&gorm.Session{Write: true}) - 强一致读(如刚
Create完立刻First)必须用db.Clauses(dbresolver.Write).First(&u)或db.Session(&gorm.Session{Write: true}).First(&u)
从库延迟和节点宕机需业务层兜底
dbresolver 完全不感知复制延迟,也不查 SHOW SLAVE STATUS。它把 Replicas 当静态列表,节点宕机后不会自动剔除。
关键事实:
- 从库
Seconds_Behind_Master = 12时,Find仍照常转发,导致“刚提交就查不到” - 要实现延迟感知,得额外维护从库状态 map,定期轮询各节点并调用
Resolver.ReplaceReplicas()动态刷新 - 事务内所有操作(包括
SELECT)都绑定主库连接,没法“因为从库快就切过去”——一致性优先级高于负载分担 - 不要依赖链式调用顺序控制路由,比如
db.Where().Session().Find()中Session放后面可能失效
最易被忽略的是:GORM 对延迟和故障完全无感,所有兜底逻辑(查延迟、剔节点、降级重试、缓存补偿)都得自己写,框架只管路由,不管数据是否新鲜。











