连接池配置

数据库连接是昂贵的资源。每次建立连接要经过 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
}

这段代码的问题:

  • 每次调用都创建连接,开销大
  • 高并发时连接数爆炸,数据库扛不住
  • 连接泄漏风险高

连接池解决这些问题:

  • 预先创建一批连接,按需取用
  • 用完归还池中,不真正关闭
  • 控制连接总数,保护数据库

Go 标准库的连接池

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%
  • 单实例应用:CPU 核心数 × 2 + 有效磁盘数
  • 微服务:根据实例数量均分

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

使用 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 操作。