一次 MySQL 连接池打满,我花一晚上重构了 Go 的数据库操作

写一个 Go 的 API 服务时,我犯了个低级错误:每个请求都新建一个 sql.DB 实例。上线第三天,凌晨两点,报警炸了------MySQL 连接数飙到 500+,数据库直接拒绝连接。

问题现场:连接池管理翻车

业务场景很简单:一个用户查询接口,每天大概 20 万请求。代码大概是这样的:

go 复制代码
func GetUser(id int) (*User, error) {
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        return nil, err
    }
    defer db.Close()

    var user User
    err = db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
    return &user, err
}

每个请求都 Open 一个新连接,用完 Close。看起来没问题对吧?但 sql.Open 并不会真正建立连接,它只是初始化一个连接池对象。真正建立连接是在第一次查询时。

问题在于:高并发下,每个请求都创建独立的连接池,这些连接池之间不共享连接。MySQL 默认最大连接数是 151,我这边每个连接池可能建立 3-5 个连接,20 个并发请求就能把数据库打满。

解决方案:全局连接池 + 参数调优

重构思路很简单:一个进程只维护一个 sql.DB 实例,所有请求共用这个连接池。

go 复制代码
var db *sql.DB

func InitDB(dsn string) error {
    var err error
    db, err = sql.Open("mysql", dsn)
    if err != nil {
        return err
    }

    // 关键参数调优
    db.SetMaxOpenConns(20)    // 最大打开连接数
    db.SetMaxIdleConns(10)    // 最大空闲连接数
    db.SetConnMaxLifetime(30 * time.Minute)  // 连接最大存活时间
    db.SetConnMaxIdleTime(5 * time.Minute)   // 空闲连接超时

    return db.Ping()
}

func GetUser(id int) (*User, error) {
    var user User
    err := db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
    return &user, err
}

SetMaxOpenConns 限制最大连接数,防止打满数据库。SetMaxIdleConns 控制空闲连接,避免频繁创建销毁。ConnMaxLifetime 解决长时间占用连接的问题------MySQL 默认 8 小时断开空闲连接,如果连接池不感知,会报 invalid connection

踩坑细节:参数设置不当照样翻车

第一次重构后,我以为稳了。结果第二天又报警,这次是连接泄漏。排查发现,SetConnMaxLifetime 设成了 1 分钟,导致连接频繁重建,反而增加了数据库压力。

正确做法:根据业务场景调整。我的接口平均响应时间 50ms,30 分钟的超时足够。如果业务有长事务,可以适当延长。

另一个坑:Ping() 必须在初始化时调用,否则 sql.Open 不会验证 DSN 是否正确。我第一次上线时 DSN 写错了,直到第一个请求才报错,导致服务启动无异常但接口全挂。

我们项目在重构后,把连接池配置抽成了配置项,不同环境用不同参数。生产环境 MaxOpenConns 设为 20,测试环境只开 5 个,避免测试把数据库打满。

效果数据:P99 延迟从 800ms 降到 120ms

重构上线后,监控数据对比很明显:

  • 数据库连接数:从峰值 500+ 稳定在 15-20
  • P99 延迟:从 800ms 降到 120ms
  • 错误率:从 12% 降到 0.1%

原因很简单:连接复用减少了 TCP 握手和 MySQL 认证的开销。每个连接建立需要 2-3 次网络往返,复用后这部分时间省掉了。

我们项目自研的监控系统里,还加了个连接池指标看板:当前活跃连接数、等待队列长度、连接创建速率。一旦活跃连接数超过 MaxOpenConns 的 80%,就触发告警。

最后说两句

Go 的 database/sql 包设计得很好,但文档里那些参数说明太官方了。实际调优时,得结合自己的业务场景:读多写少?短查询还是长事务?并发量多大?这些决定了 MaxOpenConnsConnMaxLifetime 怎么配。

如果刚开始写 Go 的数据库操作,记住一句话:全局只有一个 sql.DB,别在函数里创建。

相关推荐
夕除2 小时前
spring boot 13
java·mysql·spring
WAIT_TIME2 小时前
基于 Docker 快速构建 MySQL InnoDB Cluster 高可用集群与 Router 读写分离
mysql·docker·集群·innodb cluster
Full Stack Developme2 小时前
SQL like 与 正则 区别
数据库·sql·mysql
我是一颗柠檬3 小时前
【MySQL全面教学】MySQL多表查询与JOIN Day6(2026年)
数据库·后端·sql·mysql
今天背单词了吗9803 小时前
MySQL InnoDB引擎八大核心特性详解(高频面试题)
java·数据库·mysql
我也不曾来过14 小时前
MYSQL 使用C语言链接
数据库·mysql
摇滚侠4 小时前
Docker 安装 MySQL 8
mysql·docker·容器
牛马鸡niumasi4 小时前
Mysql:事务管理(上)
数据库·mysql
天海华兮4 小时前
【优】B+树,Mysql优化 慢查询 执行计划 优化表结构 避免死锁 大量插入数据大数据后果
b树·mysql·死锁·慢查询·优化表结构·大量插入数据