连接池枯竭主因是连接未归还、未活够或未入池,而非参数过小;MaxOpenConns应≤数据库max_connections的70%,MaxIdleConns设为前者的20%~30%,并必配SetConnMaxLifetime防静默断连。

连接池枯竭不是配置数字小了,而是连接没还、没活够、或根本没进池子。
db.SetMaxOpenConns 和 db.SetMaxIdleConns 怎么设才不崩
这两个值不是越大越好,也不是拍脑袋定的。MaxOpenConns 是数据库服务器能承受的硬上限,超过它会导致后端拒绝新连接;MaxIdleConns 是你愿意为“等活干”的连接付多少内存成本。
-
MaxOpenConns应 ≤ 数据库max_connections的 70%(比如 PostgreSQL 默认 100,这里设 70);若用连接池代理(如 PgBouncer),可适当提高,但需同步调高代理层限制 -
MaxIdleConns推荐设为MaxOpenConns * 0.2–0.3(例如 MaxOpenConns=70 → MaxIdleConns=15~21),空闲太多会占内存,太少则每次高峰都得重建连接 - 必须配
db.SetConnMaxLifetime(time.Hour):避免连接因数据库侧超时(如 MySQLwait_timeout=600)被静默断开,导致后续Exec或Query报driver: bad connection
连接没归还?查 defer db.Close() 和 context 超时漏网点
连接泄漏最常见于没显式释放,或 panic 后 defer 没执行。Gin 中尤其容易在中间件或异步 goroutine 里丢掉 rows.Close() 或事务未 Commit()/Rollback()。
- 所有
db.Query()后必须defer rows.Close(),哪怕只取一行;rows.Next()返回 false 后不关,连接就卡在池里 - 用
db.BeginTx(ctx, nil)时,ctx要传到底层操作;若 handler 中用了context.WithTimeout但事务没响应 cancel,该连接会一直占用直到ConnMaxLifetime到期 - 别在 goroutine 里直接用
*sql.DB——它线程安全,但上下文取消和错误传播会失效;应把context.Context和*sql.Tx一起传入
为什么加了连接池,还是频繁报 “sql: database is closed”
这不是连接池问题,是 *sql.DB 实例被提前关了。Gin 项目里常发生在热更新、测试 teardown 或多实例误共享时。
- 检查是否在
init()或全局变量中多次调用sql.Open(),又对某个实例调了db.Close(),导致其他地方还在用的句柄失效 - endless 或 graceful shutdown 场景下,
db.Close()必须在所有 HTTP 请求真正结束之后再触发;否则正在跑的db.QueryRowContext()可能撞上关闭中的连接 - 单元测试中用
testify/suite时,SetupTest()创建新*sql.DB,但Shutdown()被调了两次,第二次就关掉了本该活着的实例
Redis 连接池要额外防 “假空闲”
Redis 连接比 MySQL 更容易因网络抖动或服务端 timeout 配置变短而“活着但不能用”,这时 MaxIdle 里的连接看似可用,实则一借就错。
- 务必启用
TestOnBorrow:比如TestOnBorrow: func(c redis.Conn, t time.Time) error { _, err := c.Do("PING"); return err },借出前探活 -
IdleTimeout要 略小于 Redis 服务端的timeout(如 Redistimeout 300,客户端设IdleTimeout 240 * time.Second),否则空闲连接会在服务端先被 kill,客户端还不知道 - 避免在 Gin 中间件里用
redis.Pool.Get()后不Put()——尤其是带defer conn.Put()的写法,若中间件 panic,Put不会执行,连接就泄露了
真正难的不是设几个数字,而是让每条连接从创建、使用到归还的路径上,没有一个环节会意外中断或跳过清理。连接池不会自动兜底,它只负责复用——前提是,你真把连接还回来了。











