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

系统挂了

现象:用户打开页面,一直转圈。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 配置 -->
<Resource name="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 | grep 1521 | grep ESTABLISHED | wc -l
# 结果:200+

# 2. 查看应用连接池状态(JMX)
# busy: 100, idle: 0

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

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

修复:

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

排查技巧:开启 removeAbandoned 和 logAbandoned,定位泄露点。

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. 对比正常时段和异常时段
// 连接池监控代码
@Component
public class ConnectionPoolMonitor {
    
    @Scheduled(fixedRate = 60000) // 每分钟记录一次
    public void monitor() {
        DataSource ds = getDataSource();
        if (ds instanceof org.apache.tomcat.jdbc.pool.DataSource) {
            org.apache.tomcat.jdbc.pool.DataSource tomcatDS = 
                (org.apache.tomcat.jdbc.pool.DataSource) ds;
            
            PoolStats stats = new PoolStats();
            stats.setActive(tomcatDS.getActive());
            stats.setIdle(tomcatDS.getIdle());
            stats.setSize(tomcatDS.getSize());
            stats.setWaitCount(tomcatDS.getWaitCount());
            stats.setTimestamp(new Date());
            
            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 * * ?")
public void batchProcess() {
    // 1. 限制单次处理数量
    int batchSize = 1000;
    
    // 2. 分批处理,每批之间释放连接
    for (int i = 0; i < totalCount; i += batchSize) {
        try {
            processBatch(i, batchSize);
        } catch (Exception e) {
            log.error("批次处理失败", e);
            // 失败后继续下一批,而不是一直占用连接
        }
        // 每批完成后短暂休眠,让其他线程有机会获取连接
        Thread.sleep(100);
    }
}

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

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

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

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

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

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

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

代码泄漏点定位:

// 嫌疑一:手动获取Connection后没有在finally中关闭
Connection conn = session.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
// rs、ps、conn 都没有关闭!

// 嫌疑二:事务未正常提交/回滚
DBUtil.BeginTrans(false);
// 如果中间抛异常,EndTrans没调用,连接不归还

// 嫌疑三:大数据导出ResultSet流式读取未关闭
ResultSet result = 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 连接池配置
MongoClientOptions options = MongoClientOptions.builder()
    .connectionsPerHost(100)          // 每个主机的最大连接数
    .minConnectionsPerHost(10)        // 最小连接数
    .threadsAllowedToBlockForConnectionMultiplier(5)
    .connectTimeout(5000)             // 连接超时
    .socketTimeout(30000)             // Socket超时
    .maxWaitTime(10000)               // 最大等待时间
    .build();

MongoClient client = new MongoClient(
    Arrays.asList(
        new ServerAddress("192.168.1.1", 27017),
        new ServerAddress("192.168.1.2", 27017),
        new ServerAddress("192.168.1.3", 27017)
    ),
    options
);

连接池配置大全

Tomcat JDBC Pool

<Resource name="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

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

HikariCP(推荐)

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

诊断工具

1. 查看数据库连接数

-- Oracle 查看当前连接
SELECT username, count(*) FROM v$session 
WHERE username IS NOT NULL 
GROUP BY username;

-- 查看具体连接信息
SELECT sid, serial#, username, machine, program, status 
FROM v$session 
WHERE username = 'APP_USER'
ORDER BY status;

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

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

3. 查看线程堆栈

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

4. 数据库级排查

-- Oracle 查看锁
SELECT 
    object_name, 
    session_id,
    oracle_username,
    os_user_name,
    locked_mode
FROM v$locked_object lo, dba_objects do
WHERE lo.object_id = do.object_id;

-- 查看长时间运行的SQL
SELECT 
    sid, serial#, username, 
    ROUND(elapsed_seconds/60, 2) as minutes,
    sql_text
FROM v$session s, v$sql q
WHERE s.sql_id = q.sql_id
    AND s.status = 'ACTIVE'
    AND s.username IS NOT NULL
ORDER BY elapsed_seconds DESC;

常见原因总结

原因表现解决方案
代码未归还连接活跃连接数持续上升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% 就告警
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐