gin本身不提供读写分离能力,因为它是http路由框架,不涉及数据库连接层,gin.context中无db字段,也不封装sql.db或gorm.db;读写分离需在handler中手动调用独立的主从sql.db实例实现。

为什么 Gin 本身不提供读写分离能力
Gin 是 HTTP 路由框架,它不碰数据库连接层,gin.Context 里没有 DB 字段,也不封装 sql.DB 或 *gorm.DB。所谓“Gin 做读写分离”,其实是你在 Gin 的 handler 里手动调用不同 *sql.DB 实例——Gin 只负责把请求转给你,剩下的全得自己组织。
必须用两个独立 *sql.DB 实例,不能共用一个
常见错误是只开一个 sql.Open,然后靠改 DSN 地址临时切库。这会导致连接池混乱、超时参数冲突、甚至复用已关闭的连接。正确做法是显式初始化主从两套连接池:
-
masterDB, err := sql.Open("mysql", "user:pass@tcp(10.0.1.10:3306)/mydb"),配SetMaxOpenConns(30)、SetWriteTimeout(2 * time.Second) -
slaveDB, err := sql.Open("mysql", "user:pass@tcp(10.0.2.20:3306)/mydb"),配SetMaxOpenConns(120)、SetReadTimeout(8 * time.Second) - 两者必须各自
defer db.Close(),尤其在 CLI 工具或测试中容易漏掉 - 别用
masterDB.Clone()或浅拷贝结构体——*sql.DB是运行时状态对象,不是配置模板
在 Gin handler 里怎么选库:强一致性读必须走主库
不是所有 SELECT 都能扔给从库。典型场景如“下单前查余额”“支付回调验单”,必须读刚写入的数据,否则会因主从延迟导致逻辑错误。这时不能依赖“自动路由”,而要显式指定:
- 写操作:
masterDB.Exec("INSERT ...") - 强一致性读:
masterDB.QueryRow("SELECT balance FROM users WHERE id = ?", uid) - 普通读(列表、详情):
slaveDB.Query("SELECT * FROM articles LIMIT 20") - 事务内严禁混用:
tx, _ := masterDB.Begin()后,所有tx.QueryRow和tx.Exec必须来自同一tx,不能穿插slaveDB.QueryRow
容易被忽略的三个坑
最常出问题的地方不在代码结构,而在边界条件和运维信号:
- 从库
Ping()失败时,不要静默 fallback 到主库——这会让主库流量突增,应标记isHealthy = false并报警,同时让读请求失败或降级为缓存 -
Seconds_Behind_Master > 30不是连接异常,而是数据延迟,GORM 和database/sql都不感知这个指标,需单独查SHOW SLAVE STATUS并结合上下文禁用该从库 - HTTP handler 中忘了
context.WithTimeout,从库慢查询拖死整个请求,而主库连接池却空闲——读写分离不是性能银弹,超时和限流仍得各自配











