强一致读必须显式指定主库,GORM不感知复制延迟,需用Clauses(dbresolver.Write)或Session({Write: true})强制走主库;带锁SQL、高延迟从库需业务层主动探测降级;各从库连接池参数须独立配置并健康检查。

强一致读必须显式指定主库,不能依赖“刚写完就自动读”
MySQL 主从复制延迟是物理现实,GORM 完全不感知 Seconds_Behind_Master,也不会重试、等待或自动降级。你看到“刚 Create 完立刻 First 就查不到”,不是 bug,而是没控制好读写时序。
正确做法是主动声明读意图:
-
db.Clauses(dbresolver.Write).Where("id = ?", id).First(&u)—— 强制走主库,适用于详情页、支付前查余额等场景 -
db.Session(&gorm.Session{Write: true}).First(&u)—— 效果等价,但注意必须放在链式调用最开头,Where().Session().First()会失效 - 避免在事务外用
slaveDB.QueryRow查刚写入的数据,尤其不能混进事务上下文里
Raw SQL 带锁语义时默认走从库,会直接报错
Raw("SELECT * FROM users WHERE id = ? FOR UPDATE") 看似是 SELECT,但实际触发写锁。GORM 不解析 SQL 文本,只按方法名路由,所以它被当成普通读操作发到从库,立刻触发 ERROR 1290 (HY000): The MySQL server is running with the --read-only option。
这类语句必须显式切主库:
- 加
.Clauses(dbresolver.Write):db.Clauses(dbresolver.Write).Raw("SELECT ... FOR UPDATE").Scan(&u) - 或改用
db.Session(&gorm.Session{Write: true})包裹整个调用链 - 同理,
SELECT ... LOCK IN SHARE MODE、INSERT INTO ... SELECT也需同样处理
从库延迟高时 GORM 不会自动兜底,业务层必须自己探测和降级
GORM 的 dbresolver 把 Replicas 当静态列表用,节点宕机不剔除,延迟飙升也不感知。上线后如果只靠配置不加监控,很容易出现“从库 lag > 30s,所有读请求还在打过去”的情况。
可行的兜底方案包括:
- 启动时或定时轮询各从库
SHOW SLAVE STATUS,提取Seconds_Behind_Master,维护一个健康列表 - 读请求前检查延迟阈值(如 >5s),超限则自动 fallback 到主库:
if slaveIsLaggy() { db = masterDB } - 用
dbresolver.ReplaceReplicas()动态替换副本列表,配合外部探活服务(如 Consul 或自建 HTTP 健康端点) - 对关键路径(如用户中心首页)强制主库读,不走从库,避免体验断层
连接池参数必须为每个从库单独配置,否则负载不均或连接泄漏
多个从库共用同一套连接池参数(比如都设 SetMaxOpenConns(50))会导致部分节点 CPU 飙高、连接数暴涨,另一些却闲置——因为 GORM 不做负载均衡调度,只按注册顺序轮询或随机选节点。
每个从库实例要独立调优:
-
slaveDB1.DB().SetMaxOpenConns(120)、slaveDB2.DB().SetMaxOpenConns(80)—— 根据机器规格差异调整 - 必须设
SetConnMaxLifetime(10 * time.Minute),防止 MySQL 侧wait_timeoutkill 连接后 Go 端仍复用 - 首次使用前调用
slaveDB.Ping(),失败则标记不可用,避免静默 fallback 到主库压垮它
延迟问题从来不在 GORM 配置里,而在你是否愿意为每个从库单独测连通性、单独设超时、单独做健康标记。漏掉任意一环,所谓“读写分离”就只是把压力从主库转移到了某个慢从库上。


















