gorm v2需通过dbresolver插件显式配置主从连接池,主库用于写操作及事务内所有操作,从库用于默认查询;强一致读需手动指定write策略,主从延迟问题须由业务层兜底处理。

gorm.Open 时怎么配主从连接池
读写分离不是靠插件自动识别 SQL 类型,而是靠你在 gorm.Open 阶段显式声明主库(write)和从库(read)的连接池。GORM v2 官方不内置读写分离逻辑,得靠 gorm.io/plugin/dbresolver 插件配合使用。
常见错误是只传一个 gorm.Config,没注册 resolver;或者把主从 DSN 写反,导致写操作打到从库报 ERROR: cannot execute INSERT in a read-only transaction。
实操要点:
- 主库用
gorm.Open初始化一次,作为基础*gorm.DB - 调用
db.Use(dbresolver.Register(...))注册 resolver 插件 -
dbresolver.Register的sources放主库,replicas放从库(支持多个) - 每个连接池建议独立配置
MaxOpenConns和MaxIdleConns,从库连接数通常可设更高
什么时候走主库、什么时候走从库
默认规则很简单:带写语义的操作(Create、Save、Update、Delete、Transaction)强制走 sources(主库),其余查询类方法(Find、First、Where + Scan)默认走 replicas(从库)。
立即学习“go语言免费学习笔记(深入)”;
但容易被忽略的是:事务内所有操作都走主库,哪怕你只做 SELECT;另外 Raw 查询默认也走从库,如果里面写了 INSERT 就会失败。
实操建议:
- 需要强一致读(比如刚写完立刻查),用
db.Session(&session).First(...)显式指定 session 并设置AllowGlobalUnlock: true - 手动切库用
db.Clauses(dbresolver.Read).Find(...)或db.Clauses(dbresolver.Write).Exec(...) - 避免在
Where().Select().Scan()链式调用中间混入Session,顺序错会导致策略失效
从库延迟导致数据不一致怎么办
这不是 GORM 能解决的问题,而是架构层必须面对的现实:MySQL 主从复制有毫秒到秒级延迟,GORM 不做任何延迟补偿或重试。你看到的“读不到最新数据”,大概率是业务没处理好读写时序。
典型场景:用户注册(写主库)→ 立即跳转个人页(读从库)→ 查不到刚写的记录。这时候不能怪插件,得加兜底逻辑。
可行方案:
- 关键路径写完后,用主库再查一次(
db.Clauses(dbresolver.Write).First(...))确认落库 - 对非实时要求高的页面,加短缓存(如 Redis)并设置合理过期时间,避开从库抖动
- 不要依赖从库做唯一性校验(如查邮箱是否存在),这类必须走主库
- 监控从库延迟(
SHOW SLAVE STATUS的Seconds_Behind_Master),超阈值时临时降级读主库
dbresolver 插件的几个硬限制
它不是万能路由中间件,底层仍受限于 GORM 的链式构建机制和 SQL 解析能力。很多你以为能自动分流的场景,其实根本不会触发切换。
例如:db.Table("users").Select("count(*)").Row().Scan(&n) 这种原生查询,默认走从库,但如果你在 Select 里写了子查询含 FOR UPDATE,GORM 不识别,照样发给从库然后报错。
还有几个实际踩过的坑:
- 自定义
Callbacks(比如软删除钩子)若在BeforeCreate里又做了查询,该查询不会继承当前上下文的读写倾向,可能意外走从库 -
Count方法默认走从库,但某些数据库(如 TiDB)对COUNT加锁行为不同,需验证是否真能下推 - 插件不感知 context 超时,主库慢查询拖住整个请求时,从库连接池可能被耗尽
- 升级 GORM 版本前务必检查
dbresolver兼容性,v1.3.x 和 v2.2.x 的注册方式完全不同
真正难的从来不是配通读写分离,而是厘清哪些读可以接受延迟、哪些写之后的读必须等主库、以及出问题时怎么快速切回单点。这些没法靠插件自动判断,得靠业务代码里的明确标记和防御性检查。










