直接用sql.db做读写分离会出问题,因其不感知主从角色,事务中误用从库导致查不到刚写数据,且sql.tx绑定后无法切换连接。

为什么直接用 sql.DB 做读写分离会出问题
Go 标准库的 sql.DB 本身不感知主从角色,它只管连接池和执行语句。如果你手动拼接两个 sql.DB(一个连主库、一个连从库),然后靠业务代码判断“该读还是该写”,很容易在事务中误用从库——比如事务里先 INSERT 再 SELECT,结果 SELECT 走了从库,查不到刚写的记录(主从延迟或未同步)。更隐蔽的问题是:事务开始后,sql.Tx 绑定的是创建它的那个 sql.DB,你没法中途切换底层连接。
gorm.io/gorm 的 Resolver 接口怎么真正启用主从路由
GORM v2 提供了 resolver 扩展点,但默认不开启读写分离;必须显式配置 Resolver 实现并传入 gorm.Config。关键不是“加了 resolver 就自动分流”,而是它只在调用 Session 或使用 WithContext 携带特定 key 时才触发路由逻辑。
- 必须用
db.Session(&gorm.Session{Write: true})强制走主库,否则普通Find/Create仍可能被路由到从库 - 读操作默认走从库,但
First、Take等带FOR UPDATE的查询,需手动加Session(&gorm.Session{Write: true}),否则从库报错 -
Resolver的Resolve方法接收*gorm.Statement,可通过stmt.SQL.String()判断是否含INSERT/UPDATE/DELETE,但注意预编译后 SQL 是占位符,得用stmt.Clauses或stmt.Settings更可靠
主从切换失败的三个典型信号
不是所有连接异常都意味着要切主——GORM 的健康检查和故障转移是两回事。常见误判场景:
- 从库连接超时(
dial tcp 10.0.1.2:3306: i/o timeout)只影响读,不应触发主库接管全部流量;但若业务把读请求降级到主库又没限流,主库可能被打爆 - 主库返回
ERROR 1205 (40001): Deadlock found是事务冲突,不是宕机,不该触发主从切换 - 从库复制延迟超过阈值(如
Seconds_Behind_Master > 30)时,GORM 无原生检测能力,需自己查SHOW SLAVE STATUS并结合gorm.Session动态禁用该从库
用 pgxpool.Pool + 自定义 QueryFunc 实现轻量级读写分离(非 ORM 场景)
如果不用 GORM,直接用 pgx/v5 或 mysql 驱动,可以绕过框架约束,自己控制连接选择。核心是封装一层 DB 结构体,暴露 Query 和 Exec 方法,并在内部根据上下文决定用哪个连接池。
示例关键逻辑:
type DB struct {
master *pgxpool.Pool
slaves []*pgxpool.Pool
}
func (d *DB) Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) {
if isWriteQuery(sql) {
return d.master.Query(ctx, sql, args...)
}
// 轮询选一个可用 slave
for _, slave := range d.slaves {
if err := slave.Ping(ctx); err == nil {
return slave.Query(ctx, sql, args...)
}
}
return d.master.Query(ctx, sql, args...) // 降级
}
注意:isWriteQuery 不能只靠字符串前缀匹配(比如 "SELECT"),要跳过注释和换行;推荐用正则 ^\s*(INSERT|UPDATE|DELETE|REPLACE|WITH.*?INSERT|WITH.*?UPDATE),且忽略大小写。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











