必须显式引入dbresolver插件并调用db.Use(dbresolver.Register(...))注册主从连接池,否则读写分离不生效;GORM按方法语义路由(非SQL解析),事务内全走主库,从库延迟需业务层自行兜底。

直接用 gorm.Open 初始化单个 DB 并不能启用读写分离;必须显式引入 dbresolver 插件并正确注册主从连接池,否则所有操作默认走主库,或因配置错误导致写入从库报错 ERROR: cannot execute INSERT in a read-only transaction。
dbresolver.Register 必须在 gorm.Open 之后调用
很多人把 db.Use(dbresolver.Register(...)) 放在 gorm.Open 前,结果插件根本没生效。GORM 要求插件必须绑定到已初始化的 *gorm.DB 实例上。
-
db, err := gorm.Open(dialector, &gorm.Config{})是第一步,且dialector只用于初始化基础连接(通常填主库 DSN,但此时它还不承担实际路由) - 紧接着必须调用
db.Use(dbresolver.Register(...)),否则Sources和Replicas配置不会被加载 - 如果漏掉这一步,
Find、First等方法仍全部走主库——你以为启用了读写分离,其实只是“假分离”
主库写、从库读不是自动识别 SQL 类型,而是按方法语义硬规则
GORM 不解析 SQL 文本判断读写,而是根据调用的方法名和上下文决定路由目标。这意味着你不能靠 Raw("SELECT ...") 就走从库,也不能靠 Where(...).Update(...) 自动切回主库。
- 写操作:
Create、Save、Update、Delete、Transaction强制走Sources(主库) - 读操作:
Find、First、Take、Count、Scan默认走Replicas(从库) - 事务内所有操作(包括
SELECT)都绑定到开启事务的主库连接,无法中途切换 -
Raw查询默认走从库,若里面含INSERT或UPDATE,会直接报只读错误
从库延迟高时,GORM 不会自动降级或重试
MySQL 主从复制延迟是客观存在的,GORM 本身不感知 Seconds_Behind_Master,也不会因为从库慢就自动切回主库读。业务层必须自己兜底。
立即学习“go语言免费学习笔记(深入)”;
- 不要依赖 “GORM 自动处理延迟”,它连查
SHOW SLAVE STATUS的能力都没有 - 需要强一致读(比如刚
Create完立刻First),必须显式加db.Session(&gorm.Session{Write: true}) - 若要实现延迟感知负载均衡,得额外维护一个从库状态列表,定期轮询各节点
Seconds_Behind_Master,并在选库前过滤掉延迟 >10s 或不可达的实例 - 手动切库用
db.Clauses(dbresolver.Write).First()或db.Clauses(dbresolver.Read).Update(),但后者风险极高,慎用
连接池参数必须为每个源单独配置
dbresolver 注册的每个 Sources 和 Replicas 都是独立的 gorm.Dialector,它们背后对应各自的 *sql.DB 连接池。全局设置(如 SetMaxOpenConns)只影响主库,从库连接数默认极小,容易成为瓶颈。
- 每个
mysql.Open(...)或postgres.Open(...)应该包装成带连接池配置的 dialector - 推荐做法:用
gorm.Open初始化主库后,对每个从库 DSN 单独调用sql.Open+gorm.WithContext构建自定义 dialector - 从库连接数通常可设为主库的 2–3 倍,但需配合实际并发读流量压测调整
- 忘记配从库连接池上限,会导致大量 goroutine block 在
acquireConn,表现就是读请求大面积超时
最易被忽略的是:事务里混用普通 Find 和 Session 会导致路由策略失效——比如 tx.Session(...).First() 中的 Session 参数会被忽略,因为事务对象 tx 已绑定主库连接,不再受 resolver 控制。


















