gin 默认不处理多数据源并发问题,因其仅为http框架,不感知数据库连接池、事务隔离或数据源路由;并发读写压力由gorm的单个*gorm.db实例承担,需通过dbresolver插件实现读写分离,并分别配置主从库连接池参数。

为什么 Gin 默认不处理多数据源并发问题
Gin 本身只是 HTTP 路由和中间件框架,它不感知数据库连接池、事务隔离或数据源路由。所有并发读写压力最终都落在 GORM 的 *gorm.DB 实例上——而单个 *gorm.DB 默认共享一个连接池,多个 goroutine 并发调用 Create/First 等方法时,底层会复用连接池中的空闲连接,但不会自动做读写分离或库路由。
常见误判是:「Gin 启动了多个协程,所以 GORM 就能自动分库」。错。Gin 的每个请求确实跑在独立 goroutine,但如果你只初始化了一个 DB 全局变量,所有请求都在争抢同一套连接池和同一张表。
怎么让 GORM 管理多个 MySQL 实例(读写分离)
核心不是改 Gin,而是用 GORM 自带的 Database Resolver 插件,配合自定义 resolver 实现读写分离。官方支持开箱即用,无需第三方包。
- 先分别初始化主库(写)和从库(读)的
*gorm.DB:
// 主库(写)
master, _ := gorm.Open(mysql.Open("root:pass@tcp(10.0.0.1:3306)/app?..."), &gorm.Config{})
// 从库(读)
slave, _ := gorm.Open(mysql.Open("root:pass@tcp(10.0.0.2:3306)/app?..."), &gorm.Config{})
- 然后用
dbresolver注册多数据源:
import "gorm.io/plugin/dbresolver"
r := dbresolver.Register(dbresolver.Config{
// 主库写,从库读
Sources: []gorm.Dialector{master.Dialector},
Replicas: []gorm.Dialector{slave.Dialector},
// 读操作自动路由到 replica,除非显式指定 Session
TraceResolver: true,
})
- 最后把 resolver 注入主
*gorm.DB:
db, _ := gorm.Open(mysql.Open("dummy"), &gorm.Config{})
db.Use(r)
这样,db.Find(&u) 会自动走从库;db.Create(&u) 一定走主库;db.Session(&gorm.Session{Write: true}).Find(&u) 可强制读走主库。
并发下事务与上下文传递容易踩的坑
当一个 Gin 请求里开启事务(比如转账),又想在中间件或子函数里复用该事务,必须显式传 *gorm.DB 实例,不能依赖全局 DB 变量。
-
gin.Context里存的是 HTTP 上下文,不是 GORM 事务上下文 -
DB.WithContext(c.Request.Context())不会绑定事务,只会传递 cancel/timeout - 正确做法:在事务开始后,用
db.Session(&gorm.Session{AllowGlobalUpdate: false})创建新实例,并通过参数或c.Set()传下去
例如:
func transferHandler(c *gin.Context) {
tx := db.Begin()
defer func() {
if r := recover(); r != nil { tx.Rollback() }
}()
// 把 tx 实例传给业务逻辑,而不是用全局 db
err := doTransfer(tx, c.PostForm("from"), c.PostForm("to"), c.PostForm("amount"))
if err != nil {
tx.Rollback()
c.JSON(400, gin.H{"error": err.Error()})
return
}
tx.Commit()
c.Status(200)
}
连接池配置不当会导致并发瓶颈
默认 GORM 连接池最大连接数是 10,对中高并发服务远远不够,且未区分读写连接池。实际部署必须手动调优:
- 主库连接池建议设为
SetMaxOpenConns(50),避免写操作排队 - 从库连接池可设更高(如 100),因为读请求更轻量、更频繁
- 务必调
SetMaxIdleConns和SetConnMaxLifetime,否则空闲连接长期占用、DNS 变更后无法自动重连
示例:
sqlDB, _ := db.DB() sqlDB.SetMaxOpenConns(50) sqlDB.SetMaxIdleConns(20) sqlDB.SetConnMaxLifetime(time.Hour)
注意:SetMaxOpenConns 是整个连接池上限,不是每核上限;Go 的 net/http 服务器本身已做 goroutine 复用,GORM 连接池才是真正的并发闸口。
最常被忽略的一点:不同数据源的连接池要分别调用 SetMaxOpenConns。如果你只对主库调了,从库还是用默认 10,那读请求一多就会卡在获取连接上——现象是写正常、读超时,查日志却没报错。











