连接池失效——高并发下的隐形杀手
2026/5/16 7:21:08 网站建设 项目流程

连接池失效——高并发下的隐形杀手

系统挂了

现象:用户打开页面,一直转圈。5分钟后,页面报错。

错误日志

org.apache.tomcat.jdbc.pool.PoolExhaustedException: [http-nio-8080-exec-72] Timeout: Pool empty. Unable to fetch a connection in 30 seconds, none available[size:100; busy:100; idle:0; lastwait:30000]

诊断:连接池满了。100个连接全部被占用,没有空闲连接可用。

这不是第一次,也不是最后一次。

连接池是什么

连接池是数据库连接的"蓄水池":

应用线程 → 从连接池取连接 → 执行SQL → 归还连接到池 ↓ ┌──────────────┐ │ 连接池 │ │ ┌──┐┌──┐┌──┐│ │ │c1││c2││c3││ ← 预先创建好的数据库连接 │ └──┘└──┘└──┘│ └──────────────┘

正常情况:线程用完连接就归还,连接池循环利用。

故障情况:线程拿了连接不归还,连接池逐渐枯竭。

故障案例

案例1:数据库重启,连接池"假死"

处理因数据库重启而失效的中间件连接池

场景:Oracle 数据库例行重启后,应用无法访问数据库。

排查

  1. 数据库已正常启动
  2. 应用服务器已重启
  3. 但应用还是报连接超时

根因:连接池中的连接是数据库重启前创建的。数据库重启后,这些连接已经失效(TCP 连接断开)。但连接池不知道连接已失效,继续把坏连接分配给应用。

解决方案

<!-- Tomcat JDBC Pool 配置 --><Resourcename="jdbc/datasource"auth="Container"type="javax.sql.DataSource"factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"maxActive="100"maxIdle="30"minIdle="10"initialSize="10"<!--关键配置:连接有效性检查-->validationQuery="SELECT 1 FROM DUAL" validationQueryTimeout="5" testOnBorrow="true"<!-- 取连接时检查 -->testWhileIdle="true"<!-- 空闲时检查 -->timeBetweenEvictionRunsMillis="30000" minEvictableIdleTimeMillis="60000"<!-- 数据库重启后自动重建连接 -->removeAbandoned="true" removeAbandonedTimeout="60" logAbandoned="true" />

教训:连接池必须配置连接有效性检查,否则数据库重启就是一场灾难。

案例2:连接池泄露——代码忘了归还

异地平台连接池故障分析及处理(注释掉连接放入容器中) 异地平台连接池泄露问题解决与测试

现象:系统运行一段时间后变慢,最终崩溃。

排查

# 1. 查看连接数netstat-an|grep1521|grepESTABLISHED|wc-l# 结果:200+# 2. 查看应用连接池状态(JMX)# busy: 100, idle: 0

根因:代码中获取连接后,在异常分支没有归还。

// 错误代码:连接泄露publicvoidprocessData(){Connectionconn=null;try{conn=dataSource.getConnection();// 业务处理...// 这里抛出异常thrownewBusinessException("处理失败");}catch(Exceptione){// 只记录日志,没有归还连接!log.error("处理失败",e);// 缺少:conn.close();}}

修复

// 正确代码:try-with-resources 自动归还publicvoidprocessData(){try(Connectionconn=dataSource.getConnection()){// 业务处理...}catch(Exceptione){log.error("处理失败",e);}// 自动归还连接!}

排查技巧:开启removeAbandonedlogAbandoned,定位泄露点。

removeAbandoned=true removeAbandonedTimeout=60 logAbandoned=true

启用后日志会打印:

WARNING: Connection has been abandoned. PooledConnection[x.x.x.x:1521] StackTrace: com.xxx.dao.DataProcessor.processData(DataProcessor.java:45) com.xxx.service.DataService.handle(DataService.java:23) ...

案例3:一体化系统连接池跟踪

一体化跟踪(连接池泄露) 一体化数据连接泄露跟踪

现象:一体化系统运行几天后,连接池耗尽。

排查过程

  1. 开启连接池监控
  2. 记录每天的连接使用情况
  3. 对比正常时段和异常时段
// 连接池监控代码@ComponentpublicclassConnectionPoolMonitor{@Scheduled(fixedRate=60000)// 每分钟记录一次publicvoidmonitor(){DataSourceds=getDataSource();if(dsinstanceoforg.apache.tomcat.jdbc.pool.DataSource){org.apache.tomcat.jdbc.pool.DataSourcetomcatDS=(org.apache.tomcat.jdbc.pool.DataSource)ds;PoolStatsstats=newPoolStats();stats.setActive(tomcatDS.getActive());stats.setIdle(tomcatDS.getIdle());stats.setSize(tomcatDS.getSize());stats.setWaitCount(tomcatDS.getWaitCount());stats.setTimestamp(newDate());log.info("连接池状态: active={}, idle={}, size={}, wait={}",stats.getActive(),stats.getIdle(),stats.getSize(),stats.getWaitCount());// 如果活跃连接数超过阈值,告警if(stats.getActive()>tomcatDS.getMaxActive()*0.8){alertService.sendAlert("连接池告警",stats.toString());}}}}

发现:某个定时任务在周末数据量大时执行慢,连接占用时间长。

修复

// 优化:增加超时控制@Scheduled(cron="0 0 2 * * ?")publicvoidbatchProcess(){// 1. 限制单次处理数量intbatchSize=1000;// 2. 分批处理,每批之间释放连接for(inti=0;i<totalCount;i+=batchSize){try{processBatch(i,batchSize);}catch(Exceptione){log.error("批次处理失败",e);// 失败后继续下一批,而不是一直占用连接}// 每批完成后短暂休眠,让其他线程有机会获取连接Thread.sleep(100);}}

案例4:MyBatis 连接池管理问题

一体化问题处理(连接占完) 一体化系统性能分析(推测是mybatis连接池管理问题)

现象:连接池满了,但代码看起来没问题(都用了 try-with-resources)。

根因:项目用的 MyBatis 自带连接池(POOLED),poolMaximumActiveConnections只配了10个,高峰期远远不够。同时嵌套事务的传播行为配置不当,导致连接占用时间过长。

排查过程——先判断是池太小还是泄漏:

原因特征排查方式
连接池太小高峰期定时耗尽,重启后正常,负载下来后也正常调大连接池观察
连接泄漏越来越严重,即使低峰也耗尽检查代码是否忘关连接

临时把poolMaximumActiveConnections从 10 调到 30 观察一周,依然耗尽——确认是连接泄漏

代码泄漏点定位

// 嫌疑一:手动获取Connection后没有在finally中关闭Connectionconn=session.getConnection();PreparedStatementps=conn.prepareStatement(sql);ResultSetrs=ps.executeQuery();// rs、ps、conn 都没有关闭!// 嫌疑二:事务未正常提交/回滚DBUtil.BeginTrans(false);// 如果中间抛异常,EndTrans没调用,连接不归还// 嫌疑三:大数据导出ResultSet流式读取未关闭ResultSetresult=DBUtil.getBigResult(session,mapperClass,methodName,params);// 导出完没关result,Statement和Connection都占着

修复方案

  1. 导出逻辑的 finally 块中确保关闭 ResultSet 和 Statement
  2. 封装 try-with-resources 安全获取连接的方法
  3. BeginTrans/EndTrans 增加30秒超时保护,超时写入 warn 日志

MyBatis POOLED vs 生产级连接池

功能MyBatis POOLEDDruid/HikariCP
连接泄漏检测有(强制回收超时连接)
慢SQL记录
连接池监控有(JMX/SQL面板)
动态调整不支持支持

长期方案:换用 Druid/HikariCP,removeAbandoned参数可自动回收泄漏连接。

解决:调大连接池、调整事务传播行为、缩短事务范围。同时在导出逻辑 finally 中确保关闭 ResultSet,BeginTrans/EndTrans 加超时告警,封装 try-with-resources 安全方法。

案例5:MongoDB 连接池

mongodb连接池修改(改为连接集群,使用配置文件)

场景:MongoDB 从单机升级到集群,连接池配置需要调整。

// MongoDB 连接池配置MongoClientOptionsoptions=MongoClientOptions.builder().connectionsPerHost(100)// 每个主机的最大连接数.minConnectionsPerHost(10)// 最小连接数.threadsAllowedToBlockForConnectionMultiplier(5).connectTimeout(5000)// 连接超时.socketTimeout(30000)// Socket超时.maxWaitTime(10000)// 最大等待时间.build();MongoClientclient=newMongoClient(Arrays.asList(newServerAddress("192.168.1.1",27017),newServerAddress("192.168.1.2",27017),newServerAddress("192.168.1.3",27017)),options);

连接池配置大全

Tomcat JDBC Pool

<Resourcename="jdbc/datasource"type="javax.sql.DataSource"factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"maxActive="100"<!--最大活跃连接数-->maxIdle="30"<!-- 最大空闲连接数 -->minIdle="10"<!-- 最小空闲连接数 -->initialSize="10"<!-- 初始连接数 -->maxWait="30000"<!-- 最大等待时间(ms) -->validationQuery="SELECT 1 FROM DUAL" validationQueryTimeout="5" testOnBorrow="true" testOnReturn="false" testWhileIdle="true" timeBetweenEvictionRunsMillis="30000" minEvictableIdleTimeMillis="60000" removeAbandoned="true" removeAbandonedTimeout="60" logAbandoned="true" jdbcInterceptors="ConnectionState;StatementFinalizer;SlowQueryReport(threshold=1000)" />

Druid Pool

<beanid="dataSource"class="com.alibaba.druid.pool.DruidDataSource"><propertyname="url"value="jdbc:oracle:thin:@localhost:1521:orcl"/><propertyname="username"value="user"/><propertyname="password"value="pass"/><propertyname="initialSize"value="10"/><propertyname="minIdle"value="10"/><propertyname="maxActive"value="100"/><propertyname="maxWait"value="30000"/><propertyname="validationQuery"value="SELECT 1 FROM DUAL"/><propertyname="testOnBorrow"value="true"/><propertyname="testWhileIdle"value="true"/><propertyname="timeBetweenEvictionRunsMillis"value="60000"/><propertyname="removeAbandoned"value="true"/><propertyname="removeAbandonedTimeout"value="60"/><propertyname="logAbandoned"value="true"/><!-- 慢SQL监控 --><propertyname="filters"value="stat,wall,log4j"/></bean>

HikariCP(推荐)

spring:datasource:hikari:maximum-pool-size:50minimum-idle:10idle-timeout:300000max-lifetime:600000connection-timeout:30000connection-test-query:SELECT 1 FROM DUALleak-detection-threshold:60000

诊断工具

1. 查看数据库连接数

-- Oracle 查看当前连接SELECTusername,count(*)FROMv$sessionWHEREusernameISNOTNULLGROUPBYusername;-- 查看具体连接信息SELECTsid,serial#, username, machine, program, statusFROMv$sessionWHEREusername='APP_USER'ORDERBYstatus;

2. 查看应用连接池(JMX)

# 通过 JMX 查看 Tomcat 连接池jconsole localhost:1099# → MBeans → Catalina → DataSource → connectionPool

3. 查看线程堆栈

# 找出哪些线程在等待连接jstack<pid>|grep-A20"POOL EXHAUSTED"jstack<pid>|grep-A20"borrowConnection"

4. 数据库级排查

-- Oracle 查看锁SELECTobject_name,session_id,oracle_username,os_user_name,locked_modeFROMv$locked_object lo,dba_objectsdoWHERElo.object_id=do.object_id;-- 查看长时间运行的SQLSELECTsid,serial#, username,ROUND(elapsed_seconds/60,2)asminutes,sql_textFROMv$sessions,v$sqlqWHEREs.sql_id=q.sql_idANDs.status='ACTIVE'ANDs.usernameISNOTNULLORDERBYelapsed_secondsDESC;

常见原因总结

原因表现解决方案
代码未归还连接活跃连接数持续上升try-with-resources
事务过长连接占用时间长缩小事务范围
慢SQL连接执行慢,排队优化SQL、加索引
数据库重启连接全部失效testOnBorrow
并发突增连接不够用增大连接池、限流
连接泄露活跃连接慢慢增加removeAbandoned
死锁连接互相等待优化锁顺序
网络抖动连接假死连接超时检测

经验教训

1. 连接池不是越大越好

maxActive=100 满了 → 改成 200 还是满了 → 改成 500 数据库扛不住了

连接池越大,数据库压力越大。根本问题是:为什么连接占用时间这么长?

2. 一定要配置连接有效性检查

testOnBorrow=true validationQuery=SELECT 1 FROM DUAL

没有这个配置,数据库重启一次,系统就要重启一次。

3. 一定要开启泄露检测

removeAbandoned=true logAbandoned=true

生产环境必须开启,否则连接泄露只有到系统崩溃时才发现。

4. 监控比预防更重要

连接池问题很难在测试环境复现(并发不够)。生产环境必须监控:

  • 活跃连接数
  • 等待连接数
  • 连接获取时间
  • 连接关闭时间

5. 慢SQL是连接池的头号杀手

一条慢 SQL 执行 10 秒,如果有 100 个连接,只能同时处理 10 个请求,剩下的 90 个排队。

先优化慢 SQL,再调整连接池。

最后的话

连接池问题是"隐形杀手"——平时不出问题,一出就是大问题。

日志里多次出现的"连接池泄露"、“连接占完”、“连接池故障分析”,每次都是系统快挂了才发现。

事后分析原因都很简单:代码少了个close(),事务范围太大了,SQL 太慢了。但查出来往往要花半天。

最有效的方案是:

  1. 代码层面:try-with-resources(Java 7+ 的自动资源管理)
  2. 配置层面:testOnBorrow + removeAbandoned
  3. 监控层面:连接池指标实时监控,超过 80% 就告警

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询