本文针对历代文学网(点连接可进入)的Druid数据源调优实战总结分享给大家!
历代文学网数据库数据量高达1TGB,收录来自古今中外 200多个朝代和国家的作者超 15万人,诗、词、曲、赋、文言文等作品数超 114万个,成语超 5万个,名句超 12万条,名言超 130万条,著作超 2万部。
在历代文学项目稳定上线前,曾多次经历数据库宕机重启后,Druid连接池无法自动恢复,除非手动重启应用程序,才能让Druid连接池自动恢复正常!
1. Druid实战场景简介
Druid是Java语言中最好的数据库连接池。Druid能够提供强大的监控和扩展功能。
Druid是一个开源项目,源码托管在github上,源代码仓库地址是:
https://github.com/alibaba/druid
1.1 Druid数据源介绍
阿里巴巴的Druid是一个JDBC组件,它包含三部分:DruidDriver代理、DruidDataSource数据库连接池和SQLParser。Druid是阿里巴巴的开源项目,该项目主要是为了监控数据库连接池的性能指标,提供可视化的操作界面。
Druid连接池的优点:
-
可以监控数据库池的状态,包括池的状态及每个活动连接的详细状态。
-
可以提供SQL监控功能,可以监控SQL的执行时间、执行次数、执行频率等。
-
可以提供数据库密码加密功能,提高系统安全性。
-
支持数据库分表分库,提供异常连接处理机制,提高系统稳定性。
1.2 你是否遇到数据库因故障重启后,druid却无法自动恢复?
实际项目中,你是否经常会遇到以下问题:
如下日志所示,数据库宕机,重启恢复后,druid连接池却依然死翘翘,无法自动恢复,如下错误日志所示:
[2024-08-26 08:42:22,997][ c.a.d.p.DruidDataSource][ERROR][onPool-Create-2020751256][ > ] create connection SQLException, url: jdbc:postgresql://172.16.10.80:5432/cloud-platform?useUnicode=true&tcpKeepAlive=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&reWriteBatchedInserts=true, errorCode 0, state 08001
org.postgresql.util.PSQLException: Connection to 172.16.10.80:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.
at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:346) ~[postgresql-42.7.3.jar!/:42.7.3]
at org.postgresql.core.ConnectionFactory.openConnection(ConnectionFactory.java:54) ~[postgresql-42.7.3.jar!/:42.7.3]
at org.postgresql.jdbc.PgConnection.<init>(PgConnection.java:273) ~[postgresql-42.7.3.jar!/:42.7.3]
at org.postgresql.Driver.makeConnection(Driver.java:446) ~[postgresql-42.7.3.jar!/:42.7.3]
at org.postgresql.Driver.connect(Driver.java:298) ~[postgresql-42.7.3.jar!/:42.7.3]
at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:132) ~[druid-1.2.23.jar!/:?]
at com.alibaba.druid.filter.FilterAdapter.connection_connect(FilterAdapter.java:764) ~[druid-1.2.23.jar!/:?]
at com.alibaba.druid.filter.FilterEventAdapter.connection_connect(FilterEventAdapter.java:33) ~[druid-1.2.23.jar!/:?]
at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:126) ~[druid-1.2.23.jar!/:?]
at com.alibaba.druid.filter.stat.StatFilter.connection_connect(StatFilter.java:244) ~[druid-1.2.23.jar!/:?]
at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:126) ~[druid-1.2.23.jar!/:?]
at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1687) ~[druid-1.2.23.jar!/:?]
at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1803) ~[druid-1.2.23.jar!/:?]
at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2914) [druid-1.2.23.jar!/:?]
Caused by: java.net.ConnectException: Connection refused
at java.base/sun.nio.ch.Net.pollConnect(Native Method) ~[?:?]
at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:682) ~[?:?]
at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:549) ~[?:?]
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:592) ~[?:?]
at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327) ~[?:?]
at java.base/java.net.Socket.connect(Socket.java:752) ~[?:?]
at org.postgresql.core.PGStream.createSocket(PGStream.java:243) ~[postgresql-42.7.3.jar!/:42.7.3]
at org.postgresql.core.PGStream.<init>(PGStream.java:98) ~[postgresql-42.7.3.jar!/:42.7.3]
at org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:136) ~[postgresql-42.7.3.jar!/:42.7.3]
at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:262) ~[postgresql-42.7.3.jar!/:42.7.3]
数据库宕机,再恢复数据库启动后,应用服务器的druid连接池,因为没有重启服务器,导致迟迟不能恢复正常?这里一定要重启应用服务器才能让druid恢复正常吗?难道真的是Druid自己存在的bug,导致数据库宕机恢复后,自己却不能恢复么?还是我们没有学会使用Druid数据源的核心配置导致的?
答案显然不是Druid自身的bug,肯定是我们自己没用对导致的!想象一下也知道,别个都在一线大型互联网项目实战那么久了,岂能因你使用有误而被轻易推翻的?
1.3 搞懂Druid这几个核心参数用法很关键
本文重点介绍Druid数据源的如下几个关键的核心参数,搞懂它,一定让你真正玩好项目!不会再因为上述问题而烦恼:
- validationQuery
- testWhileIdle
- minEvictableIdleTimeMillis
- timeBetweenEvictionRunsMillis
- keepAlive
- keepAliveBetweenTimeMillis
2. Druid连接池6个核心参数详解
Druid数据源其实非常优秀的数据源,带有PreparedStatement缓存机制,性能非常高!但经常因为我们自己用不好,或是没有搞懂关于它的一些核心配置,从而导致恶性事件重复不止!
认真阅读本文,会让你接触上述困惑,只要搞懂这八个核心参数的组合使用,你不会再遇到上述druid连接池瘫痪的情况!
2.1 validationQuery
官方解释:用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
请注意,这个参数必须配置,而且配置了才会让testOnBorrow、testOnReturn、testWhileIdle这几个参数生效。如果不配,上述数据库宕机事故,druid连接池依然无法自动恢复正常的!
2.2 validationQueryTimeout
官方解释:单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法。
该参数是配合上一个validationQuery一起使用的,不能太大,太大,会让检测时间太久,连接池恢复正常耗时很长!设置1-3秒比较合适。
2.3 testWhileIdle
官方解释:申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。建议配置为true,不影响性能,并且保证安全性。
官方实现代码如下:
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
int notFullTimeoutRetryCnt = 0;
for (; ; ) {
// handle notFullTimeoutRetry
DruidPooledConnection poolableConnection;
try {
poolableConnection = getConnectionInternal(maxWaitMillis);
} catch (GetConnectionTimeoutException ex) {
if (notFullTimeoutRetryCnt < this.notFullTimeoutRetryCount && !isFull()) {
notFullTimeoutRetryCnt++;
if (LOG.isWarnEnabled()) {
LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
}
continue;
}
throw ex;
}
if (testOnBorrow) {
boolean validated = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validated) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validated connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
} else {
if (poolableConnection.conn.isClosed()) {
discardConnection(poolableConnection.holder); // 传入null,避免重复关闭
continue;
}
if (testWhileIdle) {
final DruidConnectionHolder holder = poolableConnection.holder;
long currentTimeMillis = System.currentTimeMillis();
long lastActiveTimeMillis = holder.lastActiveTimeMillis;
long lastExecTimeMillis = holder.lastExecTimeMillis;
long lastKeepTimeMillis = holder.lastKeepTimeMillis;
if (checkExecuteTime
&& lastExecTimeMillis != lastActiveTimeMillis) {
lastActiveTimeMillis = lastExecTimeMillis;
}
if (lastKeepTimeMillis > lastActiveTimeMillis) {
lastActiveTimeMillis = lastKeepTimeMillis;
}
long idleMillis = currentTimeMillis - lastActiveTimeMillis;
if (idleMillis >= timeBetweenEvictionRunsMillis
|| idleMillis < 0 // unexcepted branch
) {
boolean validated = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validated) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validated connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
}
}
}
if (removeAbandoned) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
poolableConnection.connectStackTrace = stackTrace;
poolableConnection.setConnectedTimeNano();
poolableConnection.traceEnable = true;
activeConnectionLock.lock();
try {
activeConnections.put(poolableConnection, PRESENT);
} finally {
activeConnectionLock.unlock();
}
}
if (!this.defaultAutoCommit) {
poolableConnection.setAutoCommit(false);
}
return poolableConnection;
}
}
这个就按官方建议来吧,配置为true!
2.4 minEvictableIdleTimeMillis
官方解释:连接保持空闲而不被驱逐的最小时间,即最小生存时间!见如下代码:
for (; i < poolingCount; ++i) {
DruidConnectionHolder connection = connections[i];
if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) {
keepAliveConnections[keepAliveCount++] = connection;
continue;
}
if (checkTime) {
if (phyTimeoutMillis > 0) {
long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
if (phyConnectTimeMillis > phyTimeoutMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
if (idleMillis < minEvictableIdleTimeMillis
&& idleMillis < keepAliveBetweenTimeMillis) {
break;
}
if (idleMillis >= minEvictableIdleTimeMillis) {
if (i < checkCount) {
evictConnections[evictCount++] = connection;
continue;
} else if (idleMillis > maxEvictableIdleTimeMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis
&& currentTimeMillis - connection.lastKeepTimeMillis >= keepAliveBetweenTimeMillis) {
keepAliveConnections[keepAliveCount++] = connection;
} else {
if (i != remaining) {
// move the connection to the new position for retaining it in the pool.
connections[remaining] = connection;
}
remaining++;
}
} else {
if (i < checkCount) {
evictConnections[evictCount++] = connection;
} else {
break;
}
}
}
销毁连接时,当检测到当前连接的最后活动时间和当前时间差(即连接的空闲时间)大于该值时,关闭当前连接。
连接的空闲时间大于 minEvictableIdleTimeMillis(连接保持空闲而不被驱逐的最小时间), 则进行回收。
2.5 timeBetweenEvictionRunsMillis
官方解释:
有两个含义:
- Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
- testWhileIdle的判断依据,详细看testWhileIdle属性的说明。
正确理解:
- 如果testWhileIdle为true,距离上次激活时间超过timeBetweenEvictionRunsMillis,则进行连接有效性检测,即执行validationQuery检测连接是否有效。
- 连接销毁线程(DestroyConnectionThread)该线程主要工作是将空闲及无效的连接销毁,可以通过timeBetweenEvictionRunsMillis 时间设置执行间隔。每次回收都是从connects 头部开始遍历;
DestroyConnectionThread线程主要回收以下几类连接:
- 连接的空闲时间大于 minEvictableIdleTimeMillis(连接保持空闲而不被驱逐的最小时间), 则进行回收。
- 大于minIdle 部分的连接会被回收。保证连接池空闲连接不会太多。
- 检查连接活跃度,不健康的连接则关闭。默认不检查,可以通过 druid.keepAlive 打开连接的健康检查。
2.6 keepAliveBetweenTimeMillis
官方解释:单位毫秒,若连接空闲时间大于keepAliveBetweenTimeMillis毫秒,执行一次有效性检测,检测不通过的连接会被销毁。
官方代码如下:
protected void createAndStartDestroyThread() {
destroyTask = new DestroyTask();
if (destroyScheduler != null) {
long period = timeBetweenEvictionRunsMillis;
if (period <= 0) {
period = 1000;
}
destroySchedulerFuture = destroyScheduler.scheduleAtFixedRate(destroyTask, period, period,
TimeUnit.MILLISECONDS);
return;
}
String threadName = "Druid-ConnectionPool-Destroy-" + System.identityHashCode(this);
destroyConnectionThread = new DestroyConnectionThread(threadName);
destroyConnectionThread.start();
}
从上述代码可知,参数timeBetweenEvictionRunsMillis仅仅是销毁线程DestroyConnectionThread的运行周期!
Druid数据库连接池中还有一个销毁连接的线程,上述已提到,名叫DestroyConnectionThread,该线程会每间隔timeBetweenEvictionRunsMillis的时间执行一次DestroyTask任务来销毁连接,这些被销毁的连接可以是存活时间达到最大值的连接,也可以是空闲时间达到指定值的连接。
如果还开启了保活机制,那么空闲时间大于keepAliveBetweenTimeMillis的连接都会被校验一次有效性,校验不通过的连接会被销毁。
因此keepAliveBetweenTimeMillis一定要大于timeBetweenEvictionRunsMillis,前者是连接保持存货的最小生命值(毫秒),而后者是检测该生命值的线程的运行时间间隔!
3. 项目实战中Druid连接池的配置
以下是项目实战中的Druid连接池可靠性配置,可以保证数据库宕机重启后,应用无需重启都能很快恢复正常响应状态!根据实际需要,只需要修改maxActive值即可!
# 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
initialSize: 8
# 连接池最小空闲的连接数
minIdle: 8
# 连接池最大“活跃”连接数量,当连接数量达到该值时,再获取新连接时,将处于等待状态,直到有连接被释放,才能借用成功
maxActive: 512
# 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
maxWait: 30000
# 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。实际项目中建议配置成true
keepAlive: true
# 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validationQuery: "select 'x'"
# 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
validationQueryTimeout: 3
# 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: false
# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
# 默认自动提交
default-auto-commit: true
# 默认只读
default-read-only: false
# 默认事务隔离
default-transaction-isolation: 4
# 销毁连接时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接(配置连接在池中的最小生存时间)
# 配置一个连接在池中最小生存的时间(连接保持空闲而不被驱逐的最小时间),单位是毫秒
minEvictableIdleTimeMillis: 1800000
# 有两个含义:
# 1) Destroy线程会检测连接的间隔时间(即Druid数据库连接池有一个销毁连接的线程会每间隔timeBetweenEvictionRunsMillis执行一次DestroyTask#run方法来销毁连接),如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
# 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
timeBetweenEvictionRunsMillis: 15000
# 空闲时间大于30秒,执行一次有效性检测,检测不通过的连接会被销毁
# Druid数据库连接池中还有一个销毁连接的线程,会每间隔timeBetweenEvictionRunsMillis的时间执行一次DestroyTask任务来销毁连接,这些被销毁的连接可以是存活时间达到最大值的连接,也可以是空闲时间达到指定值的连接。如果还开启了保活机制,那么空闲时间大于keepAliveBetweenTimeMillis的连接都会被校验一次有效性,校验不通过的连接会被销毁。
keepAliveBetweenTimeMillis: 30000
# 打开PSCache,并且指定每个连接上PSCache的大小
# 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
poolPreparedStatements: true
# 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
max-pool-prepared-statement-per-connection-size: 20
4. 总结
本文中,我们详细介绍了Druid连接池的几个核心参数配置,通过对这些配置介绍,能使我们能更准确的了解Druid,然后感受Druid的高性能,可靠性设计的迷人之处!支持国产数据源,支持Druid!