根本原因是应用未归还连接:show processlist中大量time持续增长的sleep连接,表明代码漏调close()、异常分支未释放、事务阻塞或连接池配置未匹配wait_timeout导致连接长期占用。

这不是连接池的问题,是连接没还回去
SHOW PROCESSLIST 看到一堆 Sleep 状态就基本确定了
连接池本身不会“报错”,它只是管理连接的工具;真正触发 Too many connections 的,是 MySQL 服务端连接数打满。当你执行 SHOW PROCESSLIST,看到大量 Sleep 状态、Time 列数值持续增长(比如几百秒甚至上千秒),说明这些连接被应用拿走后没归还——不是连接池配置错了,是代码里漏了 close() 或连接未被 try-with-resources / finally 保障释放。
- 常见于异常分支:SQL 执行失败后直接 return,跳过了连接关闭逻辑
- ORM 嵌套事务场景:外层事务开启后,内层调用另一个 service 方法又开了新事务,但 rollback 逻辑只覆盖了内层
- HTTP 调用阻塞在事务中:事务里发了远程请求,等十几秒才回来,这期间连接一直被占着
wait_timeout 和 interactive_timeout 设得太大,会让空闲连接赖着不走
MySQL 默认 wait_timeout=28800(8 小时),意味着一个空闲连接能挂 8 小时才被服务端主动断开。连接池通常有自己的空闲驱逐机制(如 HikariCP 的 idleTimeout),但如果这个值比 MySQL 的 wait_timeout 还大,或者连接池根本没配驱逐,那连接就会在 MySQL 侧长期处于 Sleep 状态,直到超时才释放——而这段时间它仍计入 Threads_connected 总数。
- 建议把
wait_timeout和interactive_timeout统一设为300(5 分钟)或600(10 分钟) - 确保连接池的
maxLifetime和idleTimeout都小于 MySQL 的wait_timeout,否则池子会“信任”MySQL 来清理,结果谁也不清 - 注意:改完需重启 MySQL 或执行
SET GLOBAL wait_timeout = 300(临时生效)
应用没关进程,旧连接还在占用连接数
Java 应用重启不干净、Python 脚本异常退出、K8s Pod 没等连接优雅关闭就 kill -9,都会导致连接没来得及发送 FIN 包给 MySQL。MySQL 看不到断连信号,只能靠 TCP keepalive 或自己的 wait_timeout 被动回收——这中间可能有几分钟空白期,连接数就卡在高位下不来。
- 检查
netstat -an | grep :3306 | grep ESTABLISHED | wc -l,如果远高于SHOW STATUS LIKE 'Threads_connected',说明存在半开连接 - K8s 场景务必配置
preStophook,用sleep 10给连接留出释放时间 - Java 应用上线前加 JVM 参数
-Dsun.net.client.defaultConnectTimeout=5000 -Dsun.net.client.defaultReadTimeout=5000,避免网络卡顿拖住连接释放
真正难排查的不是“连接数高”,而是“为什么高了还不掉”。重点盯住 SHOW PROCESSLIST 里 Time 值大的那些连接,顺着 Host 和 User 定位到具体服务和账号,再查对应应用日志里的 SQL 执行链路——大部分时候,问题不在数据库配置,而在某一行忘了关连接的代码。











