dbresolver.Register 必须在 gorm.Open 之后调用,否则读写分离不生效;主从库需独立 sql.DB 实例并校验连通性;事务内操作强制走主库;强一致读需显式指定 Write;从库延迟与故障需业务层主动监控和动态替换。

dbresolver.Register 必须在 gorm.Open 之后调用
插件不生效是最常见的“读写分离没起作用”原因。你写了 db.Use(dbresolver.Register()),但 GORM 根本没绑定上——因为调用时机错了。gorm.Open 返回的是一个未注册插件的裸 *gorm.DB 实例,必须先拿到它,再调用 Use。
常见错误写法是把 db.Use(...) 放在 gorm.Open 前,或者放在异步初始化里(比如 init 函数中),结果整个服务跑起来后所有 Find 还是打主库,日志里也无报错提示。
-
db, err := gorm.Open(mysql.Open(masterDSN), &gorm.Config{})是第一步,masterDSN只负责初始化基础连接,此时还不能路由 - 紧接着必须执行
db.Use(dbresolver.Register()),否则Sources和Replicas配置不会加载 - 如果用
db.Debug()观察 SQL 执行 IP,发现始终是主库地址,大概率就是这一步漏了
主库和从库必须用独立 *sql.DB 实例初始化
不能把两个 DSN 拼成一个字符串传给 gorm.Open,MySQL 驱动会直接报 invalid connection;也不能复用同一个 *sql.DB 对象去连不同地址——*sql.DB 是有状态的运行时对象,不是配置模板。
每个库都要单独 sql.Open,并各自调用 Ping() 校验连通性。失败时不 panic,后续请求就静默 fallback 到主库,压测时容易突然崩掉。
立即学习“go语言免费学习笔记(深入)”;
- 主库:
masterDB, _ := sql.Open("mysql", masterDSN)→masterDB.Ping()→masterDB.SetMaxOpenConns(20) - 从库:
slaveDB, _ := sql.Open("mysql", slaveDSN)→slaveDB.Ping()→slaveDB.SetMaxOpenConns(120) - 注册时用
dbresolver.Register().Sources(masterDB).Replicas(slaveDB),不是 DSN 字符串 - 每个
AddSlave()调用都必须检查返回的error,忽略它等于默认放弃该从库
事务内所有操作强制走主库,与 SQL 内容无关
你写 tx := db.Begin(),然后调 tx.First(&u),哪怕语句是纯 SELECT,也一定走主库。这不是 GORM “聪明地识别了事务”,而是硬编码规则:事务上下文一建立,所有 tx.Xxx() 全部绑定到开启它的那个 *sql.DB 实例。
最容易踩的坑是混用:事务里用 tx.Create(),又去调 slaveDB.QueryRow() 查数据。这不是“读从库”,这是开了两个独立会话,查不到刚写的记录,业务逻辑直接错乱。
- 事务内不要出现任何对
slaveDB或其他独立*sql.DB的调用 -
Raw("SELECT ... FOR UPDATE")默认走从库,一执行就报ERROR 1290,必须加.Clauses(dbresolver.Write) - 强一致读(如支付前查余额)不能靠“刚写完就自动读主库”,得显式写
db.Session(&gorm.Session{Write: true}).First(&u)
从库延迟和健康状态必须由业务层兜底
GORM 不查 SHOW SLAVE STATUS,也不感知 Seconds_Behind_Master。从库 lag 10 秒时,它照样把 Find 请求发过去,前端看到“刚提交就查不到”是常态,不是 bug。
也没有自动故障剔除:一个从库宕机后仍留在 Replicas 列表里,GORM 会不断重试连接,直到超时才 fallback 主库——这段时间内所有读请求都在等,QPS 直线下降。
- 需额外启动 goroutine 定期轮询各从库的
Seconds_Behind_Master,延迟 >5s 就标记为不可用 - 用
dbresolver.ReplaceReplicas()动态刷新健康从库列表,别依赖重启服务 - 连接池参数必须分开设:
slaveDB.SetReadTimeout(8 * time.Second),masterDB.SetWriteTimeout(2 * time.Second) - 从库
read_only=ON必须已在 MySQL 层开启,否则SELECT FOR UPDATE在从库执行会直接报错


















