必须用两个独立*sql.db实例实现主从分离,主库处理写和强一致读,从库仅处理最终一致性读;dbresolver需显式注册且分别配置主从dsn,否则读请求静默回退主库。

不能靠 dbresolver 插件“配完就自动分流”,它只响应显式标记或固定方法调用,不解析 SQL 语义,也不感知复制延迟;生产环境必须用两个独立 *sql.DB 实例,主库走写和强一致读,从库仅处理最终一致性读。
gorm.Open 阶段必须显式注册 dbresolver 并分离主从 DSN
插件不会自动识别你写了几个数据库地址——GORM 默认只认一个主库连接。不注册 dbresolver,所有操作都走主库;注册了但没传 replicas,读请求会静默 fallback 到主库,日志里还不报错。
-
gorm.Open只传主库 DSN 初始化基础*gorm.DB,别把从库地址拼进同一个字符串里,驱动直接报invalid connection - 立刻调用
db.Use(dbresolver.Register()),sources放主库(可多个,但通常只一个),replicas放从库列表(支持多个) - 每个从库 DSN 必须单独调用
db.AddSlave()注册,且检查返回的error——注册失败时 GORM 不 panic,后续读请求全打主库,压垮风险极高 - 从库 DSN 中的
read_only=ON必须已开启,否则SELECT FOR UPDATE在从库执行会直接报ERROR 1290 (HY000)
什么时候走主库、什么时候走从库不是靠 SQL 文本判断
GORM 的路由逻辑基于方法签名和上下文标记,不是正则匹配 "SELECT" 或 "INSERT"。你写的 Raw("SELECT ... FOR UPDATE") 默认走从库,一执行就挂;而事务内所有操作强制走主库,哪怕你只调 tx.Find()。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
- 写操作:
Create、Save、Update、Delete、Exec强制走sources(主库) - 普通读操作:
Find、First、Where().Scan()默认走replicas(从库),前提是至少一个从库注册成功且健康 - 事务内:
tx := db.Begin()后所有tx.Xxx()都走主库,与 SQL 内容无关 - 强一致读(如支付前查余额):必须显式加
db.Clauses(dbresolver.Write).First(&u)或db.Session(&gorm.Session{Write: true}).First(&u),不能依赖“刚写完就自动读主库”
连接池、超时、健康检查必须分开配置,不能复用参数
主库和从库是两种负载特征完全不同的服务端点:主库写多、延迟敏感、连接数少;从库读多、可容忍延迟、连接复用率高。共用 SetMaxOpenConns 或忽略 Ping() 校验,上线后必出问题。
- 主库:
masterDB.SetMaxOpenConns(20)、SetConnMaxLifetime(5 * time.Minute)、SetWriteTimeout(2 * time.Second),初始化后立刻masterDB.Ping(),失败直接panic - 从库:
slaveDBs[i].SetMaxOpenConns(100)、SetConnMaxLifetime(10 * time.Minute)、SetReadTimeout(8 * time.Second),首次读前异步Ping(),超时则标记isHealthy = false - 每个
*sql.DB实例都必须单独defer db.Close(),尤其在 CLI 工具或测试中容易漏掉;HTTP 服务建议在shutdown阶段统一关闭 - 不要浅拷贝
*sql.DB指针或复用sql.Open()返回值——它是有状态对象,不是配置模板
事务内混用主从库不会报错,但会导致数据不一致
最危险的不是连接失败,而是业务逻辑基于过期快照做决策:比如事务里用 slaveDB.QueryRow() 查到旧余额,再用 masterDB.Exec() 扣款,结果扣了错误金额。这种 bug 很难复现,线上才暴露。
- 一旦调用
masterDB.Begin(),后续所有操作必须基于返回的*sql.Tx实例,禁止穿插调用slaveDB.QueryRow()或封装的全局ReadDB().Query() -
tx.QueryRow()和tx.Exec()底层仍走主库连接,这是事务 ACID 的前提;跨库等于开了两个独立会话 - DAO 层若封装了
GetUserByID()方法,默认走从库,但在事务函数里无意识调用,就等于主动破坏隔离性 - GORM 的
db.Unscoped()或带clause.Locking的查询会自动退回到主库,但这不是兜底机制,而是明确语义触发——别指望它救你的误用
真正难的不是配通,而是把“强一致读”的边界划清楚:哪些场景必须走主库,哪些可以接受秒级延迟,以及当从库 Seconds_Behind_Master > 30 时,是降级、限流,还是拒绝服务——这些没法靠框架自动解决,得在业务代码里硬编码判断逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










