深入解析与解决 Druid 连接池的 GetConnectionTimeoutException 异常
在使用 Java 应用连接关系型数据库时,数据库连接池是提升性能、管理资源的关键组件。阿里巴巴开源的 Druid 是当前国内最流行、功能最强大的数据库连接池之一。然而,在实际生产环境中,许多开发者都曾遇到过这样一个令人头疼的异常:com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 60000, active 5, maxActive 5, creating 0。
这个异常直观地表明应用在尝试从 Druid 连接池获取一个数据库连接时超时了。本博客将深入剖析这个异常的背后原因,提供从诊断到解决的全套方案,并分享相关的最佳实践,帮助你彻底理解和解决这一问题。
目录#
异常信息解读#
让我们先来逐字逐句地理解这个异常信息:
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 秒后依然无果,最终超时。
异常的根本原因#
这个异常的本质是资源耗尽。具体来说,是由于数据库连接成为一种稀缺资源,其生命周期管理出现了问题。主要原因可以归结为以下几类:
- 连接泄漏(最常见):应用程序从连接池获取连接(
dataSource.getConnection())后,在使用完毕时没有正确关闭(connection.close())。在 Druid 中,close()方法实际是将连接标记为空闲并返回到池中。如果没有调用,该连接将一直被当前线程“占用”,即使相关的数据库操作早已完成。 - 慢 SQL 查询:应用程序执行的 SQL 语句效率极低,导致单个连接被长时间占用。例如,全表扫描、缺乏索引的复杂查询、数据库锁竞争等,都会显著增加连接的持有时间。
- 事务时间过长:在声明式事务(如 Spring 的
@Transactional)中,如果事务范围界定过大,包含了很多非数据库操作或慢逻辑,会导致数据库连接在整个事务期间都被占用。 - 连接池配置不合理:
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:分析代码和日志#
- 检查资源关闭:确保所有
Connection、Statement、ResultSet都在finally块或使用 Try-With-Resources 语法(Java 7+)中关闭。 错误示例: 正确示例(Try-With-Resources):Connection conn = dataSource.getConnection(); // ... 执行SQL // 忘记调用 conn.close();try (Connection conn = dataSource.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql)) { // ... 执行SQL } // 自动关闭 conn 和 stmt - 检查事务注解
@Transactional:确保事务边界合理,不要在事务方法内进行长时间的非数据库操作(如远程调用、文件 IO、复杂的计算等)。 - 分析慢 SQL:根据 Druid 监控输出的慢 SQL,使用数据库的
EXPLAIN命令分析执行计划,优化查询和索引。
解决方案与最佳实践#
4.1 临时解决方案(治标)#
- 适当增加
maxActive:如果确认不是泄漏,只是瞬时并发过高,可以适当调大最大连接数。但这不是根本解决办法,需谨慎。 - 调整
maxWait:缩短超时时间可以让应用更快地失败,避免线程长时间等待,从而保护应用不会因为数据库问题而整体雪崩。例如设置为 3-5 秒。
4.2 根本解决方案(治本)#
- 修复连接泄漏:这是最重要的一步。使用上述的泄漏检测功能定位代码,并修正关闭连接的逻辑。
- 优化慢 SQL:建立数据库索引、重写低效查询、避免
SELECT *、进行数据库分库分表等。 - 优化事务:将大事务拆分为小事务,确保
@Transactional注解只包裹必要的数据库操作。 - 合理的连接池配置:根据应用的实际情况(如 QPS、平均查询耗时)来设置连接池参数。
Druid 连接池关键配置参数详解#
| 参数 | 默认值 | 说明 | 最佳实践建议 |
|---|---|---|---|
initialSize | 0 | 连接池初始化时创建的连接数。 | 应用启动后会有预热过程,可设置为 5-10,避免第一次请求的延迟。 |
minIdle | 0 | 连接池中保持的最小空闲连接数。 | 建议设置为 initialSize 的值,避免连接池收缩后又频繁创建。 |
maxActive | 8 | 连接池最大活跃连接数。 | 核心参数。根据数据库最大连接数和应用并发量设置。通常 20-50 起步,通过压测调整。 |
maxWait | -1(无限等待) | 获取连接时最大等待时间(毫秒)。 | 必须设置。建议 3000-5000ms,避免线程无限期等待。 |
timeBetweenEvictionRunsMillis | 60000 | 关闭空闲连接的检测线程的运行周期(毫秒)。 | 默认 1 分钟即可。 |
minEvictableIdleTimeMillis | 1800000 | 一个连接在池中最小生存的时间(毫秒)。 | 默认 30 分钟,可根据情况调整。 |
validationQuery | null | 用于检测连接是否有效的 SQL 查询。 | 建议设置一个轻量级的查询,如 SELECT 1。 |
testWhileIdle | false | 建议设置为 true,在空闲时检测连接的有效性。 | 推荐 true。 |
testOnBorrow | false | 在获取连接时检测有效性。 | 不推荐 true,影响性能。依赖 testWhileIdle 即可。 |
testOnReturn | false | 在归还连接时检测有效性。 | 不推荐 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,而应该遵循“诊断 -> 定位 -> 修复”的流程:
- 首要任务:通过 Druid 监控和泄漏检测功能,确认并修复连接泄漏。
- 性能优化:分析和优化慢 SQL 与长事务。
- 合理配置:根据应用实际负载,科学地配置连接池参数,特别是
maxActive和maxWait。
养成良好的编码习惯(如使用 Try-With-Resources)和建立完善的监控体系,是预防此类问题的根本之道。