Druid连接池源码解析:为什么你的MySQL连接被偷偷回收了?
Druid连接池深度剖析:当MySQL悄悄关闭连接时,你的应用为何浑然不知?
深夜,监控系统突然告警,某个核心服务接口的响应时间从几十毫秒飙升到数秒,紧接着出现大量Communications link failure异常。你迅速登录服务器查看日志,发现错误信息中赫然写着“The last packet successfully received from the server was 658,107 milliseconds ago”——这意味着数据库连接已经空闲了超过10分钟,而MySQL服务器早已将其关闭。但奇怪的是,连接池却认为这个连接仍然有效,继续将其分配给业务线程使用。
这不是什么罕见的场景,而是许多使用Druid连接池的团队都曾踩过的坑。表面上看,这是一个简单的“连接被MySQL回收”问题,但深入Druid源码后你会发现,事情远比想象中复杂。连接池的回收逻辑、MySQL的超时机制、应用配置的微妙平衡,这三者之间的博弈决定了你的系统能否稳定运行。
1. 连接池的双重身份:资源管理者与风险承担者
连接池在现代Java应用中扮演着双重角色。一方面,它是资源管理者,负责高效地复用数据库连接,避免频繁创建和销毁带来的性能损耗。另一方面,它又是风险承担者,必须时刻警惕数据库连接的状态变化,防止将无效连接交给业务代码使用。
Druid作为阿里巴巴开源的数据库连接池,以其强大的监控功能和灵活的配置选项而闻名。但正是这种灵活性,让许多开发者在配置时感到困惑。特别是当涉及到连接回收时,Druid提供了多个看似相似但作用完全不同的参数,稍有不慎就会埋下隐患。
1.1 连接池的基本工作原理
要理解连接回收问题,首先需要了解连接池是如何管理连接生命周期的。Druid内部维护着一个连接数组,这个数组中的每个元素都代表一个物理数据库连接。连接池通过几个关键线程来管理这些连接:
- 创建线程:当连接数量不足时,负责创建新的数据库连接
- 销毁线程:定期检查连接状态,回收空闲时间过长的连接
- 保活线程:对空闲连接进行健康检查,确保其仍然有效
这些线程协同工作,确保连接池中既有足够的连接处理业务请求,又不会维持过多无用连接浪费资源。但问题在于,连接池对“连接是否有效”的判断,与数据库服务器(如MySQL)的判断标准可能存在差异。
1.2 MySQL的隐式杀手:wait_timeout与interactive_timeout
MySQL服务器有两个关键参数控制着连接的生命周期:
| 参数名 | 默认值 | 作用 |
|---|---|---|
wait_timeout | 28800秒(8小时) | 非交互式连接的空闲超时时间 |
interactive_timeout | 28800秒(8小时) | 交互式连接的空闲超时时间 |
这两个参数的含义经常被混淆。简单来说:
- 交互式连接:通过MySQL命令行客户端等工具建立的连接
- 非交互式连接:通过JDBC等程序接口建立的连接
对于大多数Java应用,我们使用的是非交互式连接,因此wait_timeout参数更为关键。当连接空闲时间超过这个阈值,MySQL服务器会主动关闭连接,释放资源。但关键在于:MySQL关闭连接时不会通知客户端。
这就导致了一个尴尬的局面:连接池认为连接还活着(因为上次使用它时一切正常),但MySQL已经单方面终止了它。当下一次业务线程尝试使用这个连接时,就会遇到“Communications link failure”异常。
2. Druid的回收机制:源码视角下的真相
要理解为什么Druid没有感知到MySQL已经关闭了连接,我们需要深入DruidDataSource类的shrink()方法。这是Druid连接回收的核心逻辑所在。
2.1 shrink()方法的核心逻辑
shrink()方法在DestroyTask线程中定期执行,它的主要职责是清理连接池中的空闲连接。让我们通过简化后的代码逻辑来理解其工作原理:
public void shrink(boolean checkTime, boolean keepAlive) {
// 获取锁,确保线程安全
lock.lockInterruptibly();
try {
if (!inited) {
return;
}
final int checkCount = poolingCount - minIdle;
final long currentTimeMillis = System.currentTimeMillis();
// 遍历连接池中的所有连接
for (int i = 0; i < poolingCount; ++i) {
DruidConnectionHolder connection = connections[i];
if (checkTime) {
// 计算连接的空闲时间
long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
// 关键判断1:如果空闲时间小于最小可驱逐时间,直接跳出循环
if (idleMillis < minEvictableIdleTimeMillis) {
break;
}
// 关键判断2:如果当前索引小于需要检查的数量,或者空闲时间超过最大可驱逐时间
if (checkTime && i < checkCount) {
evictConnections[evictCount++] = connection;
} else if (idleMillis > maxEvictableIdleTimeMillis) {
evictConnections[evictCount++] = connection;
} else if (keepAlive) {
keepAliveConnections[keepAliveCount++] = connection;
}
}
}
// 后续处理:实际销毁或保活连接
// ...
} finally {
lock.unlock();
}
}
这段代码中有几个关键点需要特别注意:
-
排序的重要性:Druid的连接数组是按照
lastActiveTime(最后活跃时间)排序的,空闲时间最长的连接排在前面。这就是为什么当遇到第一个空闲时间小于minEvictableIdleTimeMillis的连接时,可以直接break跳出循环——后面的连接空闲时间更短,肯定也不满足条件。 -
minEvictableIdleTimeMillis的真正作用:很多人误以为这个参数是“连接空闲多久后会被回收”,但实际上它是循环终止条件。当连接的空闲时间小于这个值时,Druid就停止检查后续连接。
-
maxEvictableIdleTimeMillis的兜底作用:无论连接池中有多少空闲连接,只要某个连接的空闲时间超过这个值,就会被强制回收。
2.2 默认配置的陷阱
Druid的默认配置如下:
minEvictableIdleTimeMillis: 30分钟(1,800,000毫秒)maxEvictableIdleTimeMillis: 7小时(25,200,000毫秒)timeBetweenEvictionRunsMillis: 60秒(60,000毫秒)
而MySQL的默认wait_timeout是8小时(28,800,000毫秒)。看起来似乎没有问题,因为Druid的maxEvictableIdleTimeMillis(7小时)小于MySQL的wait_timeout(8小时)。
但实际情况要复杂得多。考虑以下场景:
- 应用在凌晨2点创建了一个连接,使用后归还到连接池
- 这个连接在连接池中一直处于空闲状态
- 早上8点,业务高峰开始,连接池需要分配连接
- 此时连接已经空闲了6小时,但Druid的
maxEvictableIdleTimeMillis是7小时,所以不会回收 - MySQL的
wait_timeout是8小时,也不会关闭连接 - 看起来一切正常,对吗?
问题在于,MySQL的wait_timeout是从最后一次交互开始计算的,而Druid的lastActiveTime是从应用层最后一次使用连接开始计算的。这两个时间点可能存在差异:
- 应用在2:00使用连接执行查询,2:00:05收到结果,将连接归还
- MySQL服务器可能在2:00:10才完全处理完这个查询
- 对于MySQL来说,连接的空闲时间从2:00:10开始计算
- 对于Druid来说,连接的空闲时间从2:00:05开始计算
这5秒的差异在大多数情况下无关紧要,但在边界条件下可能成为致命问题。
3. 健康检查:连接有效性的最后防线
既然Druid无法主动感知MySQL是否关闭了连接,那么就需要通过主动检查来验证连接的有效性。Druid提供了多种健康检查机制,但每种机制都有其适用场景和性能代价。
3.1 三种健康检查模式
Druid支持三种连接有效性检查方式,通过不同的配置参数控制:
| 检查时机 | 配置参数 | 作用 | 性能影响 |
|---|---|---|---|
| 借出时检查 | testOnBorrow | 每次从连接池获取连接时执行验证查询 | 高,每次获取连接都有额外开销 |
| 归还时检查 | testOnReturn | 连接归还到连接池时执行验证查询 | 中,影响归还操作的性能 |
| 空闲时检查 | testWhileIdle | 连接在池中空闲时定期执行验证查询 | 低,在后台线程中执行 |
大多数生产环境推荐使用testWhileIdle=true,因为它能在保证连接有效性的同时,对性能影响最小。但这里有一个关键细节:testWhileIdle只在timeBetweenEvictionRunsMillis指定的时间间隔内对空闲连接进行检查。
3.2 验证查询的优化
默认情况下,Druid使用SELECT 1作为验证查询。对于MySQL,这通常是最佳选择,但需要注意一个细节:不同数据库驱动对SELECT 1的实现方式不同。
在Druid 1.2.5及以上版本中,引入了一个与MySQL驱动相关的优化:usePingMethod。当这个参数为true时(默认值),Druid会使用MySQL驱动的pingInternal()方法来检查连接,而不是执行SELECT 1。
// MySqlValidConnectionChecker中的关键代码
if (usePingMethod) {
if (conn instanceof DruidPooledConnection) {
conn = ((DruidPooledConnection) conn).getConnection();
}
if (conn instanceof ConnectionProxy) {
conn = ((ConnectionProxy) conn).getRawObject();
}
if (clazz.isAssignableFrom(conn.getClass())) {
if (validationQueryTimeout <= 0) {
validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;
}
try {
// 使用ping方法而不是执行SQL
ping.invoke(conn, true, validationQueryTimeout * 1000);
return true;
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof SQLException) {
throw (SQLException) cause;
}
throw e;
}
}
}
pingInternal()方法比执行SELECT 1更高效,因为它直接在网络层检查连接是否存活,不需要经过SQL解析和执行过程。但在某些MySQL驱动版本中,这个方法可能存在bug,导致误判连接状态。
如果你遇到“discard long time none received connection”警告,可以尝试将druid.mysql.usePingMethod设置为false:
# 在JVM启动参数中添加
-Ddruid.mysql.usePingMethod=false
# 或者在Spring Boot的application.properties中添加
spring.datasource.druid.connection-properties=druid.mysql.usePingMethod=false
3.3 健康检查的时间窗口问题
即使配置了testWhileIdle=true,仍然可能出现连接失效的问题。原因在于检查的时间窗口:
假设配置如下:
timeBetweenEvictionRunsMillis: 60000(60秒)minEvictableIdleTimeMillis: 300000(5分钟)- MySQL
wait_timeout: 300(5分钟)
时间线如下:
- 00:00:00 - 连接被创建并开始使用
- 00:00:10 - 连接归还到连接池,开始空闲
- 00:01:00 - 销毁线程第一次运行,连接空闲50秒,小于5分钟,不检查
- 00:02:00 - 销毁线程第二次运行,连接空闲1分50秒,小于5分钟,不检查
- 00:03:00 - 销毁线程第三次运行,连接空闲2分50秒,小于5分钟,不检查
- 00:04:00 - 销毁线程第四次运行,连接空闲3分50秒,小于5分钟,不检查
- 00:05:00 - 销毁线程第五次运行,连接空闲4分50秒,小于5分钟,不检查
- 00:05:10 - MySQL服务器检测到连接已空闲5分钟,关闭连接
- 00:06:00 - 销毁线程第六次运行,此时连接已被MySQL关闭,但Druid直到现在才检查
可以看到,在00:05:10到00:06:00这50秒内,连接实际上已经失效,但Druid仍然可能将其分配给业务线程使用。
注意:这里有一个常见的误解。很多人认为
testWhileIdle会定期检查所有空闲连接,但实际上Druid只检查那些空闲时间超过minEvictableIdleTimeMillis的连接。如果连接的空闲时间没有达到这个阈值,即使配置了testWhileIdle=true,也不会进行检查。
4. 实战配置:构建稳健的连接池
理解了Druid连接回收的原理后,我们可以针对性地调整配置,避免连接失效问题。以下是一套经过生产验证的配置方案,适用于大多数场景。
4.1 基础配置模板
spring:
datasource:
druid:
# 连接池基本配置
initial-size: 5
min-idle: 5
max-active: 20
# 连接有效性检查
test-while-idle: true
test-on-borrow: false # 生产环境不建议开启,性能影响大
test-on-return: false # 生产环境不建议开启
validation-query: SELECT 1
validation-query-timeout: 3
# 连接回收配置
time-between-eviction-runs-millis: 30000 # 30秒检查一次
min-evictable-idle-time-millis: 180000 # 3分钟
max-evictable-idle-time-millis: 300000 # 5分钟
# 保活机制
keep-alive: true
keep-alive-between-time-millis: 60000 # 1分钟
# 连接泄露检测(仅用于调试)
remove-abandoned: false # 生产环境不建议开启
log-abandoned: false
# MySQL特定优化
connection-properties: druid.mysql.usePingMethod=false
# 监控配置
filters: stat,wall,slf4j
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
4.2 配置参数详解与调优建议
让我们详细分析每个关键参数的作用和调优思路:
连接池大小配置
initial-size:连接池初始化时创建的连接数。建议设置为min-idle的值,避免应用启动后第一次请求需要等待连接创建。min-idle:连接池中最少保持的空闲连接数。这个值不宜过大,否则会浪费数据库资源;也不宜过小,否则在流量突增时可能来不及创建新连接。max-active:连接池中允许的最大连接数。这个值需要根据数据库的最大连接数、应用实例数、以及每个实例的并发需求来综合确定。
健康检查配置
test-while-idle:必须设置为true,这是保证连接有效性的核心机制。validation-query:使用SELECT 1即可,对于MySQL这是最轻量的检查方式。validation-query-timeout:设置一个合理的超时时间,建议3-5秒。太短可能导致误判,太长则影响连接获取速度。
连接回收配置(核心)
这里需要根据MySQL的wait_timeout来精心计算:
// 计算Druid配置与MySQL超时的关系
int mysqlWaitTimeout = 300; // MySQL wait_timeout,单位秒
int safetyMargin = 30; // 安全边际,单位秒
// Druid的最大空闲时间应该小于MySQL的超时时间减去安全边际
int maxEvictableIdleTimeMillis = (mysqlWaitTimeout - safetyMargin) * 1000;
// 最小空闲时间应该远小于最大空闲时间,确保有足够的时间窗口进行检查
int minEvictableIdleTimeMillis = maxEvictableIdleTimeMillis / 2;
// 检查频率应该足够高,确保在连接失效前能发现问题
int timeBetweenEvictionRunsMillis = Math.min(
minEvictableIdleTimeMillis / 6, // 至少检查6次
60000 // 最多1分钟检查一次
);
基于这个计算逻辑,如果MySQL的wait_timeout是300秒(5分钟),那么Druid的配置应该是:
max-evictable-idle-time-millis: 270000(4.5分钟)min-evictable-idle-time-millis: 135000(2.25分钟)time-between-eviction-runs-millis: 30000(30秒)
这样配置可以确保:
- 连接在MySQL关闭之前就会被Druid回收(4.5分钟 < 5分钟)
- 连接空闲2.25分钟后开始接受健康检查
- 每30秒检查一次,在连接失效前至少有4次检查机会
保活机制
keep-alive: 设置为true,启用保活机制keep-alive-between-time-millis: 保活检查间隔,建议设置为time-between-eviction-runs-millis的一半
保活机制是Druid 1.1.22版本引入的重要特性。当连接空闲时间超过minEvictableIdleTimeMillis但小于maxEvictableIdleTimeMillis时,Druid会对这些连接执行保活操作(执行验证查询),而不是直接关闭。这可以避免频繁创建新连接的开销。
4.3 监控与告警配置
除了正确的参数配置,完善的监控也是必不可少的。Druid提供了丰富的监控指标,可以通过JMX或内置的StatViewServlet获取。
启用监控配置:
@Configuration
public class DruidConfig {
@Bean
public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() {
ServletRegistrationBean<StatViewServlet> registrationBean =
new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
// 白名单(为空表示允许所有访问)
registrationBean.addInitParameter("allow", "");
// 黑名单
registrationBean.addInitParameter("deny", "192.168.1.100");
// 登录查看信息的账号密码
registrationBean.addInitParameter("loginUsername", "admin");
registrationBean.addInitParameter("loginPassword", "admin");
// 是否能够重置数据
registrationBean.addInitParameter("resetEnable", "false");
return registrationBean;
}
@Bean
public FilterRegistrationBean<WebStatFilter> druidWebStatFilter() {
FilterRegistrationBean<WebStatFilter> registrationBean =
new FilterRegistrationBean<>(new WebStatFilter());
// 添加过滤规则
registrationBean.addUrlPatterns("/*");
// 忽略过滤的格式
registrationBean.addInitParameter("exclusions",
"*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return registrationBean;
}
}
关键监控指标:
需要特别关注的几个指标:
| 指标 | 正常范围 | 异常处理 |
|---|---|---|
ActiveCount | 0 ~ maxActive的70% | 如果持续接近maxActive,考虑增加连接数或优化SQL |
PoolingCount | 接近minIdle | 如果远大于minIdle,可能连接泄露 |
WaitThreadCount | 0或接近0 | 如果持续大于0,说明连接不足 |
NotEmptyWaitCount | 应该很小 | 如果很大,说明获取连接经常需要等待 |
NotEmptyWaitMillis | 平均小于100ms | 如果很大,需要优化连接获取逻辑 |
告警规则建议:
- 连接泄露检测:如果
RemoveAbandonedCount在短时间内快速增长,可能代码中存在未关闭连接的情况。 - 连接池耗尽预警:当
ActiveCount持续超过maxActive的80%时发出警告。 - 连接获取超时:当
WaitThreadCount持续大于0且NotEmptyWaitMillis超过1秒时发出警告。 - 无效连接比例:通过
ConnectErrorCount和ConnectCount计算错误率,超过1%时发出警告。
5. 高级场景与疑难问题处理
即使按照最佳实践配置了Druid,在某些特殊场景下仍然可能遇到问题。这里分享几个实际案例和解决方案。
5.1 案例一:突发流量下的连接风暴
现象:某电商系统在大促期间,监控显示数据库连接数瞬间打满,大量请求超时。
分析:查看Druid监控发现,ActiveCount瞬间达到maxActive,WaitThreadCount急剧上升。进一步分析业务日志,发现某个批量处理任务没有使用连接池,而是每次操作都创建新连接。
解决方案:
- 修复代码,确保所有数据库操作都使用连接池
- 为批量任务配置独立的连接池,避免影响在线业务
- 启用连接泄露检测,临时配置
remove-abandoned: true和remove-abandoned-timeout: 120,找出未关闭连接的代码
配置调整:
# 为批量任务配置专用数据源
batch:
datasource:
druid:
initial-size: 3
min-idle: 3
max-active: 10
max-wait: 5000 # 批量任务可以等待更长时间
validation-query: SELECT 1
test-while-idle: true
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
max-evictable-idle-time-millis: 600000
5.2 案例二:长事务导致的连接不可用
现象:系统偶尔出现连接获取超时,但连接池显示有空闲连接。
分析:通过Druid的SQL监控功能,发现某些事务执行时间长达数分钟。这些长事务占用了连接,但Druid无法回收正在使用的连接。
解决方案:
- 优化SQL,减少事务执行时间
- 设置事务超时时间,避免事务无限期执行
- 对于查询操作,使用只读事务或非事务模式
代码示例:
@Service
public class OrderService {
@Autowired
private DataSource dataSource;
// 为长事务设置超时
@Transactional(timeout = 30) // 30秒超时
public void processBatchOrders(List<Order> orders) {
// 分批处理,避免单次事务过大
for (int i = 0; i < orders.size(); i += 100) {
List<Order> batch = orders.subList(i, Math.min(i + 100, orders.size()));
processOrderBatch(batch);
}
}
// 只读查询使用只读事务
@Transactional(readOnly = true)
public List<Order> findRecentOrders(Date fromDate) {
// 查询逻辑
}
// 简单查询可以不使用事务
public Order findById(Long id) {
// 直接使用JdbcTemplate,不开启事务
return jdbcTemplate.queryForObject(
"SELECT * FROM orders WHERE id = ?",
new OrderRowMapper(),
id
);
}
}
5.3 案例三:网络抖动导致的连接假死
现象:应用与数据库之间的网络偶尔抖动后,部分连接变得不可用,但Druid的健康检查没有发现问题。
分析:网络抖动可能导致TCP连接处于半开状态(half-open)。在这种状态下,连接在操作系统层面仍然是ESTABLISHED状态,但实际已经无法通信。Druid的SELECT 1检查可能因为TCP重传机制而成功,但实际业务SQL会失败。
解决方案:
- 在连接验证查询中使用更复杂的SQL,触发真正的网络通信
- 启用TCP KeepAlive机制
- 使用连接初始化SQL设置MySQL会话变量
配置示例:
spring:
datasource:
druid:
# 使用更复杂的验证查询
validation-query: SELECT 1 FROM DUAL WHERE 1=1
# 连接初始化时执行的SQL
connection-init-sqls:
- SET SESSION wait_timeout=300
- SET NAMES utf8mb4
# 连接属性,启用TCP KeepAlive
connection-properties: |
socketTimeout=30000
connectTimeout=5000
tcpKeepAlive=true
druid.mysql.usePingMethod=false
5.4 多数据源场景下的特殊考虑
在微服务架构中,一个应用可能连接多个数据库。每个数据源都应该有独立的连接池配置,因为不同数据库的负载特性和超时设置可能不同。
多数据源配置示例:
@Configuration
public class MultiDataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "primaryJdbcTemplate")
public JdbcTemplate primaryJdbcTemplate(
@Qualifier("primaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean(name = "secondaryJdbcTemplate")
public JdbcTemplate secondaryJdbcTemplate(
@Qualifier("secondaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
对应的配置文件:
spring:
datasource:
primary:
url: jdbc:mysql://primary-db:3306/app_db
username: app_user
password: ${PRIMARY_DB_PASSWORD}
druid:
initial-size: 5
min-idle: 5
max-active: 20
test-while-idle: true
validation-query: SELECT 1
time-between-eviction-runs-millis: 30000
min-evictable-idle-time-millis: 180000
max-evictable-idle-time-millis: 240000 # 主库wait_timeout=300秒
secondary:
url: jdbc:mysql://secondary-db:3306/report_db
username: report_user
password: ${SECONDARY_DB_PASSWORD}
druid:
initial-size: 3
min-idle: 3
max-active: 10
test-while-idle: true
validation-query: SELECT 1
time-between-eviction-runs-millis: 60000 # 报表库检查频率可以低一些
min-evictable-idle-time-millis: 300000
max-evictable-idle-time-millis: 420000 # 报表库wait_timeout=480秒
6. 性能测试与调优验证
配置完成后,必须通过压力测试验证连接池的稳定性和性能。以下是一个简单的测试方案:
6.1 连接泄漏测试
模拟连接未正确关闭的场景,验证Druid的removeAbandoned机制是否生效:
@SpringBootTest
class ConnectionLeakTest {
@Autowired
private DataSource dataSource;
@Test
void testConnectionLeakDetection() throws Exception {
// 临时启用连接泄露检测
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
druidDataSource.setRemoveAbandoned(true);
druidDataSource.setRemoveAbandonedTimeout(10); // 10秒
druidDataSource.setLogAbandoned(true);
// 模拟连接泄露:获取连接但不关闭
List<Connection> leakedConnections = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Connection conn = dataSource.getConnection();
leakedConnections.add(conn);
// 不执行conn.close()
}
// 等待超时
Thread.sleep(15000);
// 验证连接池状态
System.out.println("Active connections: " + druidDataSource.getActiveCount());
System.out.println("Pooling connections: " + druidDataSource.getPoolingCount());
// 清理
for (Connection conn : leakedConnections) {
try {
if (!conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
// 忽略已关闭的连接
}
}
// 恢复配置
druidDataSource.setRemoveAbandoned(false);
druidDataSource.setLogAbandoned(false);
}
}
6.2 并发性能测试
模拟高并发场景,验证连接池的吞吐量和响应时间:
@SpringBootTest
class ConcurrentAccessTest {
@Autowired
private DataSource dataSource;
@Test
void testConcurrentAccess() throws InterruptedException {
int threadCount = 50;
int iterations = 100;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger();
AtomicInteger errorCount = new AtomicInteger();
List<Long> responseTimes = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
for (int j = 0; j < iterations; j++) {
long startTime = System.currentTimeMillis();
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT 1")) {
long endTime = System.currentTimeMillis();
responseTimes.add(endTime - startTime);
if (rs.next()) {
successCount.incrementAndGet();
}
} catch (SQLException e) {
errorCount.incrementAndGet();
System.err.println("SQL error: " + e.getMessage());
}
// 随机等待,模拟真实场景
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 100));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
latch.await(2, TimeUnit.MINUTES);
executor.shutdown();
// 输出统计结果
System.out.println("Total requests: " + (successCount.get() + errorCount.get()));
System.out.println("Success: " + successCount.get());
System.out.println("Errors: " + errorCount.get());
if (!responseTimes.isEmpty()) {
double avgTime = responseTimes.stream()
.mapToLong(Long::longValue)
.average()
.orElse(0);
long maxTime = responseTimes.stream()
.mapToLong(Long::longValue)
.max()
.orElse(0);
long p95 = calculatePercentile(responseTimes, 95);
System.out.println("Average response time: " + avgTime + "ms");
System.out.println("Max response time: " + maxTime + "ms");
System.out.println("P95 response time: " + p95 + "ms");
}
}
private long calculatePercentile(List<Long> times, double percentile) {
List<Long> sorted = new ArrayList<>(times);
Collections.sort(sorted);
int index = (int) Math.ceil(percentile / 100.0 * sorted.size());
return sorted.get(index - 1);
}
}
6.3 连接有效性测试
模拟MySQL连接超时后,验证Druid是否能正确检测并恢复:
@SpringBootTest
class ConnectionValidationTest {
@Autowired
private DataSource dataSource;
@Test
void testConnectionRecovery() throws Exception {
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
// 获取初始连接
Connection conn1 = dataSource.getConnection();
System.out.println("Initial connection: " + conn1);
// 模拟连接长时间空闲(超过MySQL wait_timeout)
System.out.println("Simulating connection idle beyond MySQL timeout...");
Thread.sleep(350000); // 等待5分50秒,超过5分钟
// 尝试使用连接
try {
Statement stmt = conn1.createStatement();
ResultSet rs = stmt.executeQuery("SELECT 1");
if (rs.next()) {
System.out.println("Connection still valid (unexpected)");
}
rs.close();
stmt.close();
} catch (SQLException e) {
System.out.println("Connection invalid as expected: " + e.getMessage());
}
// 关闭旧连接
conn1.close();
// 获取新连接
Connection conn2 = dataSource.getConnection();
System.out.println("New connection: " + conn2);
// 验证新连接是否有效
Statement stmt = conn2.createStatement();
ResultSet rs = stmt.executeQuery("SELECT 1");
if (rs.next()) {
System.out.println("New connection works correctly");
}
rs.close();
stmt.close();
conn2.close();
// 检查连接池状态
System.out.println("Active count: " + druidDataSource.getActiveCount());
System.out.println("Pooling count: " + druidDataSource.getPoolingCount());
System.out.println("Destroy count: " + druidDataSource.getDestroyCount());
}
}
7. 生产环境巡检清单
最后,我整理了一份生产环境Druid连接池的巡检清单。建议定期(如每周)检查这些项目,确保连接池健康运行:
7.1 配置检查
- [ ]
maxEvictableIdleTimeMillis< MySQLwait_timeout* 1000 * 0.9 - [ ]
timeBetweenEvictionRunsMillis≤minEvictableIdleTimeMillis/ 6 - [ ]
testWhileIdle= true - [ ]
validationQuery已正确设置 - [ ]
keepAlive= true(Druid 1.1.22+) - [ ]
removeAbandoned= false(生产环境)
7.2 监控指标检查
- [ ]
ActiveCount峰值不超过maxActive的80% - [ ]
WaitThreadCount大部分时间为0 - [ ]
NotEmptyWaitMillisP95 < 100ms - [ ]
PoolingCount在minIdle附近波动 - [ ]
ConnectErrorCount增长缓慢(每天<10次)
7.3 日志分析
- [ ] 没有频繁的"discard long time none received connection"警告
- [ ] 没有"Communications link failure"错误
- [ ] 连接创建和销毁频率正常(<10次/分钟)
- [ ] 慢SQL数量在可接受范围内
7.4 定期维护任务
- [ ] 每月检查一次MySQL的
wait_timeout和interactive_timeout设置 - [ ] 每季度进行一次连接池压力测试
- [ ] 每次应用发布前,验证连接池配置与数据库配置的兼容性
- [ ] 建立连接池异常告警机制,关键指标异常时及时通知
连接池的稳定性直接关系到整个应用的可用性。通过深入理解Druid的工作原理,合理配置各项参数,建立完善的监控体系,我们可以最大限度地避免因连接问题导致的系统故障。记住,连接池不是"配置一次就忘记"的组件,它需要持续的观察和调整,才能在各种业务场景下保持最佳状态。
更多推荐



所有评论(0)