go 中需手动实现读写分离:用独立 sql.db 实例分别连接主从库,封装 dbrouter 按意图路由,事务必须全程使用主库,gorm 需正确配置 dbresolver 插件。

怎么让 sql.DB 实例自动走读库或写库
Go 里没有内置读写分离逻辑,sql.DB 本身不区分读写意图。你得自己在业务层决定用哪个 *sql.DB 实例——不是靠注释或约定,而是靠显式传参或封装后的接口。
常见错误是试图在单个 *sql.DB 上通过 SQL 前缀(比如 /*+ read_only */)触发路由,这在多数中间件(如 ProxySQL、MySQL Router)里不可靠,且 Go 的 database/sql 不解析 SQL 语义,不会帮你切库。
- 读操作必须明确调用只连从库的
*sql.DB实例,写操作同理指向主库实例 - 推荐封装一层
DBRouter结构体,提供QueryRow()、Exec()等方法,内部按调用上下文选实例 - 避免用全局变量存两个
*sql.DB后在函数里 if-else 切换——容易漏判、难测、易并发冲突
如何初始化主库和从库的 *sql.DB 并保持连接健康
主从库配置差异不只是地址不同:从库通常要设 readTimeout 更宽松,主库则需更敏感的 writeTimeout;连接池参数也该分开调优——从库查询多、生命周期短,可适当增大 MaxOpenConns;主库写压力集中,MaxIdleConns 太大会拖慢故障恢复。
容易踩的坑是共用同一个 sql.Open() 返回值再复制,但 *sql.DB 不是线程安全的“配置模板”,复制指针没意义;每个库必须独立 sql.Open() + Set*Conn* 调用。
立即学习“go语言免费学习笔记(深入)”;
- 主库初始化后务必调用
db.Ping(),失败直接 panic 或返回 error,别等第一次 query 才暴露问题 - 从库可延迟校验(比如首次读前 ping),但要配超时,避免阻塞主线程
-
SetConnMaxLifetime(10 * time.Minute)对主从都建议设置,防止长连接卡在已下线的旧节点上
事务里为什么不能切到从库
事务必须全程绑定同一数据库连接,而从库默认不接受写操作,且即使开启 super_read_only=OFF,也无法保证事务内读到刚写入的数据(主从延迟、binlog 应用滞后)。一旦你在事务中混用主库写 + 从库读,大概率遇到 ERROR 1792 (HY000): Cannot execute statement in a READ ONLY transaction 或脏读。
更隐蔽的问题是 ORM(如 GORM)自动开启事务后,你无感知地调用了封装的「读方法」,结果底层还是发到了从库——它不会因为你写了 Find() 就自动切回主库。
- 所有
Begin()/BeginTx()必须基于主库*sql.DB实例 - 事务内的
Query、QueryRow都应直接使用事务对象(*sql.Tx)的方法,而非 router 封装的读方法 - 不要给事务加 context.WithTimeout 后还试图用 router 的读方法兜底——context 取消后连接可能已释放,再取从库连接会 panic
GORM 场景下怎么避免读写分离失效
GORM v2 默认不支持多数据源路由,db.Session(&session) 或 db.Clauses(clause.ReadReplica) 这类写法只在启用了 gorm.io/plugin/dbresolver 插件后才有效,且需手动注册 resolver。很多人开了插件但没调 Register(),或者把从库配置写错字段(比如用了 Host 没写 Username),导致始终走默认主库。
另一个典型问题是用 db.Unscoped().Where(...).First() 这类链式调用时,GORM 内部可能缓存了上一次的 resolver 选择,尤其在复用 *gorm.DB 实例时。
- 确认插件已导入:
import _ "gorm.io/plugin/dbresolver",且调用了db.Use(dbresolver.Register(...)) - 从库配置必须包含完整 DSN 字段,
gorm.Config的DSN字符串不能省略密码或数据库名 - 慎用
db.Session()包裹读操作——它会覆盖 resolver 的自动判断,强制指定连接池,反而绕过读写分离
主从延迟、连接泄漏、事务跨库这些点,不跑真实流量根本看不出问题。压测时得单独对从库打读请求,看 p99 是否突增,而不是只盯着主库 CPU。










