深入解析与解决 Druid 连接池的 GetConnectionTimeoutException 异常

在使用 Java 应用连接关系型数据库时,数据库连接池是提升性能、管理资源的关键组件。阿里巴巴开源的 Druid 是当前国内最流行、功能最强大的数据库连接池之一。然而,在实际生产环境中,许多开发者都曾遇到过这样一个令人头疼的异常:com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 60000, active 5, maxActive 5, creating 0

这个异常直观地表明应用在尝试从 Druid 连接池获取一个数据库连接时超时了。本博客将深入剖析这个异常的背后原因,提供从诊断到解决的全套方案,并分享相关的最佳实践,帮助你彻底理解和解决这一问题。

目录#

  1. 异常信息解读
  2. 异常的根本原因
  3. 问题诊断与排查步骤
  4. 解决方案与最佳实践
  5. Druid 连接池关键配置参数详解
  6. 示例配置
  7. 总结
  8. 参考

异常信息解读#

让我们先来逐字逐句地理解这个异常信息:

com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 60000, active 5, maxActive 5, creating 0

  • com.alibaba.druid.pool.GetConnectionTimeoutException: 异常类型,表明是从 Druid 连接池获取连接时发生的超时异常。
  • wait millis 60000: 应用线程等待一个可用连接的最大时间,这里是 60 秒(60000 毫秒)。线程在尝试获取连接时,如果池中无可用连接,它会进入等待状态,超过这个时间仍未获得连接就会抛出此异常。
  • active 5: 当前活跃的连接数。指的是已经从连接池中取出、正在被应用程序使用的连接数量。
  • maxActive 5: 连接池允许建立的最大活跃连接数。这是连接池大小的上限。
  • creating 0: 当前正在创建中的连接数量。当连接池需要创建新连接来满足需求时,这个值会大于 0。

核心结论:异常信息清晰地告诉我们,当前所有 5 个活跃连接都被占用(active = maxActive),并且没有连接正在被创建(creating 0)。这意味着有 5 个(或更多)线程正在持有数据库连接,但都没有及时归还给连接池,导致后续需要数据库连接的线程在等待了 60 秒后依然无果,最终超时。

异常的根本原因#

这个异常的本质是资源耗尽。具体来说,是由于数据库连接成为一种稀缺资源,其生命周期管理出现了问题。主要原因可以归结为以下几类:

  1. 连接泄漏(最常见):应用程序从连接池获取连接(dataSource.getConnection())后,在使用完毕时没有正确关闭(connection.close())。在 Druid 中,close() 方法实际是将连接标记为空闲并返回到池中。如果没有调用,该连接将一直被当前线程“占用”,即使相关的数据库操作早已完成。
  2. 慢 SQL 查询:应用程序执行的 SQL 语句效率极低,导致单个连接被长时间占用。例如,全表扫描、缺乏索引的复杂查询、数据库锁竞争等,都会显著增加连接的持有时间。
  3. 事务时间过长:在声明式事务(如 Spring 的 @Transactional)中,如果事务范围界定过大,包含了很多非数据库操作或慢逻辑,会导致数据库连接在整个事务期间都被占用。
  4. 连接池配置不合理
    • maxActive 设置过小,无法支撑应用在高峰期的并发请求。
    • maxWait 设置过长或过短,影响了应用的响应或快速失败能力。

问题诊断与排查步骤#

当出现此异常时,应遵循以下步骤进行排查:

步骤 1:启用 Druid 内置的监控和 WallFilter#

Druid 提供了强大的监控功能,这是诊断问题的首选工具。

application.yml 或配置类中启用监控:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      # ... 其他配置 ...
      # 开启监控统计
      filters: stat,wall,log4j2
      filter:
        stat:
          enabled: true
          log-slow-sql: true
          slow-sql-millis: 1000
      # 开启 Web 监控页面
      web-stat-filter:
        enabled: true
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        login-username: admin # 生产环境一定要设置密码!
        login-password: admin

配置后,访问 http://你的应用地址/druid 登录后可以查看:

  • 数据源页面: 查看活跃连接数、池大小等实时状态。
  • SQL 监控: 找出执行缓慢的 SQL。
  • Web 应用: 查看 URI 请求的耗时,帮助定位问题接口。
  • 连接泄漏检测: 详见下一步。

步骤 2:启用连接泄漏检测#

Druid 可以检测哪些线程获取了连接但没有关闭。

spring:
  datasource:
    druid:
      # ... 其他配置 ...
      # 连接泄漏检测
      remove-abandoned: true
      remove-abandoned-timeout: 300 # 单位:秒。如果一个连接超过300秒未关闭,则视为泄漏
      log-abandoned: true # 输出泄漏日志,会打印堆栈信息

启用后,如果发生泄漏,应用日志中会记录详细的堆栈跟踪,明确指出是哪段代码没有正确关闭连接。

步骤 3:分析代码和日志#

  1. 检查资源关闭:确保所有 ConnectionStatementResultSet 都在 finally 块或使用 Try-With-Resources 语法(Java 7+)中关闭。 错误示例
    Connection conn = dataSource.getConnection();
    // ... 执行SQL
    // 忘记调用 conn.close();
    正确示例(Try-With-Resources)
    try (Connection conn = dataSource.getConnection();
         PreparedStatement stmt = conn.prepareStatement(sql)) {
        // ... 执行SQL
    } // 自动关闭 conn 和 stmt
  2. 检查事务注解 @Transactional:确保事务边界合理,不要在事务方法内进行长时间的非数据库操作(如远程调用、文件 IO、复杂的计算等)。
  3. 分析慢 SQL:根据 Druid 监控输出的慢 SQL,使用数据库的 EXPLAIN 命令分析执行计划,优化查询和索引。

解决方案与最佳实践#

4.1 临时解决方案(治标)#

  • 适当增加 maxActive:如果确认不是泄漏,只是瞬时并发过高,可以适当调大最大连接数。但这不是根本解决办法,需谨慎。
  • 调整 maxWait:缩短超时时间可以让应用更快地失败,避免线程长时间等待,从而保护应用不会因为数据库问题而整体雪崩。例如设置为 3-5 秒。

4.2 根本解决方案(治本)#

  1. 修复连接泄漏:这是最重要的一步。使用上述的泄漏检测功能定位代码,并修正关闭连接的逻辑。
  2. 优化慢 SQL:建立数据库索引、重写低效查询、避免 SELECT *、进行数据库分库分表等。
  3. 优化事务:将大事务拆分为小事务,确保 @Transactional 注解只包裹必要的数据库操作。
  4. 合理的连接池配置:根据应用的实际情况(如 QPS、平均查询耗时)来设置连接池参数。

Druid 连接池关键配置参数详解#

参数默认值说明最佳实践建议
initialSize0连接池初始化时创建的连接数。应用启动后会有预热过程,可设置为 5-10,避免第一次请求的延迟。
minIdle0连接池中保持的最小空闲连接数。建议设置为 initialSize 的值,避免连接池收缩后又频繁创建。
maxActive8连接池最大活跃连接数。核心参数。根据数据库最大连接数和应用并发量设置。通常 20-50 起步,通过压测调整。
maxWait-1(无限等待)获取连接时最大等待时间(毫秒)。必须设置。建议 3000-5000ms,避免线程无限期等待。
timeBetweenEvictionRunsMillis60000关闭空闲连接的检测线程的运行周期(毫秒)。默认 1 分钟即可。
minEvictableIdleTimeMillis1800000一个连接在池中最小生存的时间(毫秒)。默认 30 分钟,可根据情况调整。
validationQuerynull用于检测连接是否有效的 SQL 查询。建议设置一个轻量级的查询,如 SELECT 1
testWhileIdlefalse建议设置为 true,在空闲时检测连接的有效性。推荐 true
testOnBorrowfalse在获取连接时检测有效性。不推荐 true,影响性能。依赖 testWhileIdle 即可。
testOnReturnfalse在归还连接时检测有效性。不推荐 true,影响性能。

示例配置#

以下是一个生产环境中相对健壮的 Druid 配置示例(以 Spring Boot 的 application.yml 为例):

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: your_username
    password: your_password
    druid:
      # 初始化大小、最小、最大
      initial-size: 5
      min-idle: 5
      max-active: 20
      # 配置获取连接等待超时的时间
      max-wait: 5000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      # 校验SQL,Oracle可改为 SELECT 1 FROM DUAL
      validation-query: SELECT 1
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置监控统计和日志
      filters: stat,wall,slf4j
      # 连接泄漏检测
      remove-abandoned: true
      remove-abandoned-timeout: 300
      log-abandoned: true
      # 开启Web统计监控
      web-stat-filter:
        enabled: true
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        login-username: druid_admin
        login-password: your_strong_password_here

总结#

GetConnectionTimeoutException 是一个典型的资源管理问题信号。解决它不能简单地靠增加 maxActive,而应该遵循“诊断 -> 定位 -> 修复”的流程:

  1. 首要任务:通过 Druid 监控和泄漏检测功能,确认并修复连接泄漏
  2. 性能优化:分析和优化慢 SQL长事务
  3. 合理配置:根据应用实际负载,科学地配置连接池参数,特别是 maxActivemaxWait

养成良好的编码习惯(如使用 Try-With-Resources)和建立完善的监控体系,是预防此类问题的根本之道。

参考#

  1. Druid GitHub 仓库
  2. Druid 官方 Wiki(配置详解)
  3. Spring Boot 集成 Druid 文档