应设置连接池空闲超时与服务端wait_timeout匹配,并启用连接有效性验证;对MySQL调大wait_timeout和interactive_timeout,PostgreSQL调整tcp_keepalives_idle,同时配置连接池的ConnMaxIdleTime和ConnMaxLifetime。

连接池空闲连接被服务端主动断开怎么办
MySQL、PostgreSQL 等数据库服务端默认会在 wait_timeout(MySQL)或 tcp_keepalives_idle(PostgreSQL)超时后关闭空闲连接,而 database/sql 的连接池本身不主动探测连接有效性,导致下次复用时抛出 connection refused 或 i/o timeout 错误。
解决的关键不是“修断开”,而是让连接池在取出连接前做一次轻量级验证:
- 设置
db.SetConnMaxLifetime(5 * time.Minute):强制连接在复用前不超过指定生命周期,避免复用到服务端已丢弃的连接 - 启用
db.SetMaxIdleConns(20)和db.SetMaxOpenConns(50)后,必须配db.SetConnMaxIdleTime(3 * time.Minute)(Go 1.15+),否则空闲连接永远不清理 - 不要依赖
Ping()做连接保活——它会阻塞调用线程,且高频调用反而加重服务端负担
高并发下连接池耗尽却无法新建连接
现象是日志频繁出现 sql: connection pool exhausted,但 netstat -an | grep :3306 | wc -l 显示服务端连接数远未达上限。这通常说明连接没被正确释放,而非真的不够用。
常见漏点:
- 使用
rows, err := db.Query(...)后忘记调用rows.Close(),连接会一直卡在idle状态直到超时 - 事务中 panic 导致
tx.Commit()或tx.Rollback()未执行,该连接被 tx 持有且无法归还池中 - HTTP handler 中 defer
rows.Close()写在了错误作用域(比如写在 if 分支里),部分路径跳过关闭
建议统一用 if rows, err := db.Query(...); err != nil { ... } else { defer rows.Close() } 结构,避免遗漏。
如何安全地重试因连接断开导致的 SQL 执行失败
直接重试 db.Exec() 或 db.QueryRow() 很危险:如果第一次执行其实已成功(比如网络在返回结果前中断),重试会造成重复写入或状态错乱。
真正可重试的只有幂等操作:
- SELECT 查询可以重试,但需确保用新连接(
db.QueryRowContext(context.WithValue(ctx, "retry", true), ...)+ 连接池中间件识别并强制新建连接) - INSERT/UPDATE 建议改用带唯一约束的 upsert(如 MySQL
INSERT ... ON DUPLICATE KEY UPDATE),而非盲目重试 - 业务关键写操作应配合应用层幂等 ID(如
idempotency_key字段)+ 数据库唯一索引,由 DB 层拒绝重复
别在应用层实现“自动重连后重放 SQL”逻辑——database/sql 不提供连接上下文透传机制,你无法可靠判断哪条语句该重放、哪条已生效。
为什么加了重试逻辑还是频繁报错
很多团队加了指数退避重试,但错误率没降反升,根本原因是把“连接异常”和“SQL 执行异常”混为一谈。
典型误判:
- 把
ERROR 1205 (40001): Deadlock found when trying to get lock当成连接问题重试——这是事务冲突,应立即回滚重试整个事务,而不是等几秒再试同一条 SQL - 把
ERROR 1390 (HY000): Prepared statement contains too many placeholders当成网络抖动——这是 SQL 写法错误,重试毫无意义 - 监控只看
db.Stats().WaitCount上升就加连接数,却不查db.Stats().MaxOpenConnections是否长期打满——可能只是慢查询拖住连接,加池子只会加剧锁竞争
真正的修复入口永远是错误码本身:先分类,再定策。连接层错误(如 io: read/write timeout、connection refused)才走重连路径;SQL 层错误必须交由业务逻辑处理。

















