GORM读写分离需显式注册dbresolver插件且必须在gorm.Open之后调用,否则读操作静默走主库;它按方法名硬路由而非SQL解析,Raw("SELECT ... FOR UPDATE")默认发从库会报错;事务必须全程绑定主库实例,从库延迟与故障需业务层自行兜底。

GORM 的读写分离和分布式事务不能混为一谈——它压根不支持分布式事务,所谓“跨库事务”在 MySQL/PostgreSQL 层面就不可行;读写分离只是单机主从间的请求路由,所有事务(Begin、Commit)必须绑定到同一个 *sql.DB 实例(通常是主库),否则直接报错或产生脏数据。
dbresolver.Register 必须在 gorm.Open 之后调用
这是最常被跳过的初始化步骤。很多人把 db.Use(dbresolver.Register()) 放在 gorm.Open 前,结果插件根本没注册成功,后续所有 Find、First 全部静默走主库,还以为“分离生效了”。
-
gorm.Open只是初始化一个基础*gorm.DB实例,此时它还不知道主从在哪 - 必须紧接着调用
db.Use(dbresolver.Register(cfg)),其中cfg.Sources放主库 DSN(通常一个),cfg.Replicas放从库 DSN 列表(可多个) - 漏掉这一步,
dbresolver的switchsource和switchreplica回调压根不会注册,路由逻辑形同虚设
Raw("SELECT ... FOR UPDATE") 默认走从库会直接报错
GORM 不解析 SQL 文本,只按方法名硬路由:Create/Update 强制走 Sources,Find/First 默认走 Replicas,而 Raw 被归类为“读操作”,哪怕里面写了 FOR UPDATE。
- MySQL 从库开启
read_only=ON后,执行该语句会返回ERROR 1290 (HY000): The MySQL server is running with the --read-only option - 正确做法:强一致读 + 写语义查询,必须显式加
db.Clauses(dbresolver.Write)或db.Session(&gorm.Session{Write: true}) - 事务内所有操作(包括
tx.Raw())自动走主库,但前提是tx来自主库db.Begin(),不是从slaveDB开的
事务内混用主从库会导致 ERROR 1792 或脏读
事务必须全程使用同一物理连接,而从库默认拒绝写入,且无法保证 binlog 已同步。一旦在 tx := masterDB.Begin() 后又调用 slaveDB.QueryRow(),就脱离了事务上下文。
- 错误现象:不报错但查到过期快照,或触发
ERROR 1792 (HY000): Cannot execute statement in a READ ONLY transaction - 修复方式:事务内所有 DB 操作必须用
tx.QueryRow()、tx.Exec(),且tx只能来自主库*sql.DB实例 - 别在事务里调封装好的「读方法」——那些方法底层可能仍走
slaveDB,跟事务完全无关
从库延迟 >5s 时 GORM 不会自动降级
dbresolver 把 Replicas 当静态列表用,既不查 SHOW SLAVE STATUS,也不轮询 Seconds_Behind_Master,节点宕机后也不会自动剔除。
- 现象:从库 lag=12s,
db.Find()仍照发请求,前端看到“刚提交就查不到” - 业务层必须自己维护从库健康状态 map,定期 ping + 查询
Seconds_Behind_Master,再调用Resolver.ReplaceReplicas()动态刷新 - 连接池参数必须分开配:
masterDB.SetWriteTimeout(2 * time.Second),slaveDB.SetReadTimeout(8 * time.Second),共用会放大故障面
真正难的不是配置,而是厘清边界:GORM 的“读写分离”只解决单主多从的请求分发,它不感知复制延迟、不处理节点故障、不支持跨库事务——这些都得靠业务代码一层层兜住,否则上线就是线上事故。


















