主库和从库必须使用两个独立的*sql.db实例,主库负责事务、写入和强一致读,从库仅处理最终一致性查询;共用db实例会导致连接池错乱、健康状态无法隔离及故障互相拖累。

主库和从库必须用两个独立的 *sql.DB 实例
不是“配一个 DSN、加个插件就自动分流”,GORM 的读写分离本质是两套物理连接池。主库负责事务、写入、强一致读;从库只处理最终一致性查询。共用同一个 *sql.DB 或复用配置指针,会导致连接池参数错乱、健康状态无法隔离、故障时互相拖累。
常见错误:在 gorm.Open() 后只调一次 db.Use(dbresolver.Register(...)),却把主从 DSN 拼成一个字符串传进去 —— GORM 会静默 fallback 到主库,日志里看不到任何提示,但所有读请求实际都压在主库上。
- 主库必须单独
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()不报错,但后续读请求会 fallback 到主库 —— 必须检查返回的error,并记录告警
dbresolver 插件不识别 SQL 语义,路由靠显式意图而非 SELECT/INSERT 关键字
GORM 不会解析你写的 "SELECT ... FOR UPDATE" 或 "WITH RECURSIVE ... INSERT",它只看调用方式。默认规则简单粗暴:Create/Save/Update/Delete/Begin 强制走主库;普通 Find/First/Where().Scan() 默认走从库。但事务内所有操作(哪怕只是 tx.QueryRow())都绑定主库连接,没法中途切到从库。
容易踩坑的是 Raw 查询:写 db.Raw("SELECT * FROM users").Scan() 默认走从库,但如果里面实际是 INSERT ... SELECT,就会在从库直接报错 ERROR 1290 (HY000): The MySQL server is running with the --read-only option。
- 强一致读(如支付扣款前查余额)必须显式走主库:
db.Clauses(dbresolver.Write).First(&u)或db.WithContext(context.WithValue(ctx, "force_master", true)).First(&u) - 带锁查询(
FOR UPDATE、LOCK IN SHARE MODE)必须加db.Session(&gorm.Session{Write: true}),否则从库拒绝执行 - 不要依赖链式调用顺序控制路由,比如
db.Where(...).Session(...).Find()中Session放后面就可能失效
从库延迟和节点宕机得靠业务层兜底,GORM 不做自动剔除或重试
GORM 的 dbresolver 是静态配置,注册完就不管了。从库复制延迟 10 秒、MySQL 进程挂了、网络分区 —— 它不会感知,也不会自动降级或熔断。你看到的“刚提交就查不到”,不是框架 bug,而是架构现实。
典型现象:从库 Seconds_Behind_Master > 30,但 GORM 仍把读请求均匀打过去;或者某个从库 IP 已下线,AddSlave 返回 error 被忽略,结果所有读请求 fallback 到主库,主库 CPU 瞬间拉满。
- 启动时对每个从库做异步
Ping(),超时则标记isHealthy = false,后续跳过该节点 - 运行中定期查
SHOW SLAVE STATUS,提取Seconds_Behind_Master,超过阈值(比如 5 秒)就调resolver.ReplaceReplicas()临时剔除 - 别让读请求无限制 fallback 到主库 —— 加限流(如
golang.org/x/time/rate),避免主库被打穿 - 事务内绝对不能混用主从库句柄,
tx := masterDB.Begin()后再调slaveDB.QueryRow()是两个独立会话,查不到刚写的数据
连接池参数必须按角色分开调优,不能共用一套配置
主库和从库的负载特征完全不同:主库写多、延迟敏感、连接生命周期短;从库读多、可容忍延迟、连接复用率高。用同一套 SetMaxOpenConns 和 SetConnMaxLifetime,轻则连接泄漏,重则主库被慢查询拖垮。
常见错误是只给主库设 SetMaxOpenConns(30),然后把从库也用同一个 db 对象,导致从库连接数卡死在 30,而实际需要 100+ 并发读请求 —— 最终大量请求排队等待空闲连接,响应时间飙升。
- 主库建议
SetMaxOpenConns(20~30)、SetMaxIdleConns(10)、SetConnMaxLifetime(5 * time.Minute) - 从库建议
SetMaxOpenConns(80~120)、SetMaxIdleConns(30)、SetConnMaxLifetime(10~15 * time.Minute) - 主库
SetWriteTimeout建议设为 2~3 秒,从库SetReadTimeout可放宽到 8~10 秒 - 每个
*sql.DB实例都要单独defer db.Close(),HTTP 服务建议在 shutdown 阶段统一关闭,避免测试时漏掉
事务内所有数据库操作必须绑定同一个 *sql.Tx 对象,这个约束比任何配置都硬。一旦开始事务,就再也不能切库 —— 不是 GORM 的设计缺陷,而是 ACID 的底层要求。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











