从库必须用独立sql.Open初始化,不可复用主库*sql.DB;每个从库需单独配置SetMaxOpenConns、SetConnMaxLifetime等参数,并通过mysql.Open(dsn)构造Dialector传入dbresolver.Register()。

从库必须用单独 sql.Open 初始化,不能复用主库 *sql.DB
读写分离不是“配个 DSN 加个插件”就完事——dbresolver 注册的每个从库,背后都必须是一个独立的 *sql.DB 实例。复用主库对象、浅拷贝配置、甚至共用一个 sql.Open() 返回值,会导致连接池参数错乱、健康状态无法隔离、故障时互相拖累。
常见错误现象:db.AddSlave() 调用成功但日志里完全没报错,实际所有读请求 fallback 到主库;压测时从库连接数始终为 0,SHOW PROCESSLIST 只见主库连接。
- 每个从库都要走一遍完整的
sql.Open()+SetMaxOpenConns()+SetConnMaxLifetime()流程 - 不要把主库的
db.DB()拿过来改参数再塞给dbresolver.Register() - 从库 DSN 必须带
readTimeout=10s等超时参数,和主库的writeTimeout分开控制
SetMaxOpenConns 和 SetConnMaxLifetime 怎么设才不翻车
从库连接池不是越大越好。云数据库(如阿里云 RDS)通常每核建议 ≈20 连接,自建 MySQL 可稍宽松,但必须和主库错开:主库偏保守(例如 SetMaxOpenConns=20),从库可更激进(例如 SetMaxOpenConns=100)。
不配 SetConnMaxLifetime 是高频翻车点:云环境 NAT 或防火墙会在 3~5 分钟静默断连,下次复用直接报 read: connection reset by peer。
-
SetMaxIdleConns设为SetMaxOpenConns的 1/2~2/3(例如最大 100 → 空闲 40~60) -
SetConnMaxLifetime推荐 2~5 分钟:RDS 主备切换快,设 2 分钟更安全;自建 MySQL 可设 5 分钟 - 千万别设成
0或time.Hour:长期存活连接容易在 DB 侧积压idle in transaction,锁资源、占内存
dbresolver.Register() 里怎么传从库连接池
dbresolver.Register() 的 Replicas 字段接收的是 []gorm.Dialector,不是字符串 DSN,也不是 *sql.DB —— 你得先用从库 DSN 构造出 mysql.Open(dsn),再包进 dbresolver.Config。
容易踩的坑:dbresolver.Register(dbresolver.Config{Replicas: []gorm.Dialector{mysql.Open(slaveDSN)}}) 看似对,但如果漏掉前面的 db.Use(...),整个注册就白做;或者误把主库 mysql.Open(masterDSN) 塞进 Replicas,导致写操作打到从库报 ERROR 1290 (HY000): The MySQL server is running with the --read-only option。
- 必须在
gorm.Open()之后、db.Use(dbresolver.Register())之前,完成所有从库*sql.DB的初始化和参数设置 -
Replicas中每个mysql.Open(dsn)应该对应一个已调用过Ping()并验证连通性的从库 - 多个从库可按权重或轮询路由,但
dbresolver默认只做简单轮询,不支持自动健康检查下线
Raw 查询默认走从库,但 SQL 类型不校验
db.Raw("SELECT * FROM users").Scan() 默认走从库,但 GORM 不解析 SQL 内容。如果你不小心写了 INSERT ... SELECT 或带 FOR UPDATE 的语句,从库会直接报错,而不是自动切回主库。
强一致读场景(比如查余额后扣款)必须显式标注:db.Clauses(dbresolver.Write).First(&u),否则哪怕刚写完数据,也有可能读到旧值。
- 带锁查询(
FOR UPDATE、LOCK IN SHARE MODE)必须加db.Session(&gorm.Session{Write: true}) - 事务内所有操作(包括
tx.Raw().Scan())强制绑定主库,没法中途切到从库 - 别依赖链式调用顺序控制路由,比如
db.Where().Session().Find()中Session()放后面可能失效
真正难的不是配出两个连接池,而是让它们互不干扰地活下来:主库连接池不能被从库的慢查询拖垮,从库连接池也不能因主库故障而集体失联。每个 *sql.DB 都得自己 Ping()、自己设超时、自己管生命周期——没人替你兜底。











