数据库连接是昂贵的资源。每次建立连接要经过 TCP 握手、认证、初始化等步骤,耗时可能上百毫秒。连接池通过复用连接,避免频繁创建销毁,是性能优化的基础环节。
没有连接池时,每次数据库操作都新建连接:
func GetUser(id uint) (*User, error) {
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
defer db.DB().Close()
var user User
result := db.First(&user, id)
return &user, result.Error
}
这段代码的问题:
连接池解决这些问题:
GORM 底层使用 Go 标准库的 database/sql,连接池由它管理:
sqlDB, err := db.DB()
if err != nil {
panic(err)
}
sqlDB 就是 *sql.DB,所有连接池配置都在这个对象上。
MaxOpenConns
最大打开连接数,包括正在使用和空闲的:
sqlDB.SetMaxOpenConns(100)
设为 0 表示不限制。生产环境必须设置,否则高并发时连接数失控。
MaxIdleConns
最大空闲连接数:
sqlDB.SetMaxIdleConns(10)
空闲连接保留在池中,下次请求直接使用,避免创建开销。但占用内存,需要平衡。
ConnMaxLifetime
连接最大存活时间:
sqlDB.SetConnMaxLifetime(time.Hour)
超过这个时间,连接会被标记为过期,下次使用时关闭重建。用于:
设为 0 表示永不过期。
ConnMaxIdleTime
空闲连接最大存活时间:
sqlDB.SetConnMaxIdleTime(time.Minute * 10)
连接空闲超过这个时间会被关闭。比 ConnMaxLifetime 更精细,只针对空闲连接。
一个生产级别的连接池配置:
func InitDB() (*gorm.DB, error) {
dsn := "root:password@tcp(127.0.0.1:3306)/myapp?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(20)
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetConnMaxIdleTime(time.Minute * 30)
return db, nil
}
MaxOpenConns
不是越大越好。连接数过多会导致:
一般原则:
max_connections 的 80%MaxIdleConns
太小:频繁创建连接,延迟增加 太大:占用内存,数据库连接数压力
建议设为 MaxOpenConns 的 20%-50%。流量平稳时可以更高。
ConnMaxLifetime
必须小于数据库的 wait_timeout(MySQL)或类似设置。否则会出现 "连接已关闭" 错误。
MySQL 默认 wait_timeout 是 8 小时,建议 ConnMaxLifetime 设为 1-2 小时。
ConnMaxIdleTime
避免空闲连接占用资源。一般设为几分钟到几十分钟,取决于流量模式。
了解连接池状态,才能正确调优:
stats := sqlDB.Stats()
fmt.Printf("最大连接数: %d\n", stats.MaxOpenConnections)
fmt.Printf("当前连接数: %d\n", stats.OpenConnections)
fmt.Printf("正在使用: %d\n", stats.InUse)
fmt.Printf("空闲连接: %d\n", stats.Idle)
fmt.Printf("等待获取连接: %d\n", stats.WaitCount)
fmt.Printf("等待总时长: %v\n", stats.WaitDuration)
封装成 HTTP 接口,方便监控:
http.HandleFunc("/db/stats", func(w http.ResponseWriter, r *http.Request) {
stats := sqlDB.Stats()
json.NewEncoder(w).Encode(stats)
})
连接泄漏
症状:OpenConnections 持续增长,直到达到 MaxOpenConns
原因:获取连接后没有释放
排查:
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
rows, _ := db.Model(&User{}).Rows()
// 忘记 rows.Close()
等待超时
症状:请求卡住,WaitCount 增加
原因:连接不够用,请求排队等待
解决:
MaxOpenConns连接断开
症状:driver: bad connection 错误
原因:ConnMaxLifetime 大于数据库超时设置
解决:减小 ConnMaxLifetime
Web 服务
sqlDB.SetMaxOpenConns(50)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(30 * time.Minute)
sqlDB.SetConnMaxIdleTime(5 * time.Minute)
后台任务
sqlDB.SetMaxOpenConns(10)
sqlDB.SetMaxIdleConns(5)
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
高并发服务
sqlDB.SetMaxOpenConns(200)
sqlDB.SetMaxIdleConns(50)
sqlDB.SetConnMaxLifetime(10 * time.Minute)
sqlDB.SetConnMaxIdleTime(2 * time.Minute)
低频服务
sqlDB.SetMaxOpenConns(5)
sqlDB.SetMaxIdleConns(2)
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetConnMaxIdleTime(30 * time.Minute)
事务期间,连接会被独占:
tx := db.Begin()
// 这里的操作使用同一个连接
tx.Create(&user)
tx.Create(&order)
tx.Commit()
长事务会长时间占用连接,影响并发。尽量缩短事务时间,避免在事务中执行耗时操作。
使用 Context 可以控制连接获取超时:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var user User
err := db.WithContext(ctx).First(&user, 1).Error
如果连接池满了,等待超过 5 秒会返回超时错误。
连接池配置看似简单,但参数设置不当会严重影响性能。理解每个参数的含义,结合监控数据调优,才能发挥最佳效果。第三部分数据库连接到此结束,下一部分开始 CRUD 操作。