前言:
作为一个比较流行的数据库连接池框架,Druid以其功能强大、易于扩展而著称。
使用连接池的好处不言而喻,在之前的Mysql-java的博客中,我们看到,创建一个数据库连接不仅需要常规的三次握手,还要进行用户名密码验证,经历这么多验证之后,才算真正完成一个连接。如果我们执行一次SQL操作均要重新创建连接,那将是巨大的浪费。
故使用连接池,可以在项目启动时预先创建好一定数量的连接,执行SQL时,从连接池中获取即可,执行完毕后,将连接释放,还回连接池中。
DruidPool在创建时,需要N多参数,这些参数有些是必须项,有些是优化项,有些则是扩展项。我们就从这些参数的角度来从分析下Druid的源码。
建议可以带着问题来看Druid的源码,比如Druid是如何创建连接的,有哪些必备参数,连接如何被回收,连接的有效性如何鉴定。这样的话,我们再来看这些参数对应的源码就会恍然大悟。
1.环境准备:
mysql-server:5.7.17
mysql-connector-java:5.1.41
druid:1.1.23
2.代码准备:
DruidDataSource dataSource = new DruidDataSource();
// 以下四个参数为必输项
dataSource.setUrl("jdbc:mysql://localhost:3306/db1");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("root");
// 如果使用在项目中,建议主动调用init方法,初始化连接池
dataSource.init();
// 获取连接
Connection connection = dataSource.getConnection();
// 执行SQL
Statement stmt = connection.createStatement();
stmt.execute("select 1;");
// 获取结果集
ResultSet rs = stmt.getResultSet();
总结:在创建DruidDataSource的时候,我们定义了四个必须参数,实际这个也是我们之前使用DriverManager创建连接时的四个必须参数。这里就不再赘述。
3.连接池中连接数量相关参数
initialSize:启动连接时,在连接池中初始化的连接个数
maxActive:连接池中最多支持的活动会话个数
minIdle:回收空闲连接时,将保证至少有minIdle个连接
3.1 initialSize的使用
// DruidDataSource.init 初始化方法,在创建DruidDataSource完成后,默认初始化initialSize个连接
public void init() throws SQLException {
// 初始化状态,只会被初始化一次
if (inited) {
return;
}
...
initFromSPIServiceLoader();
resolveDriver();
initCheck();
...
// init connections
while (poolingCount < initialSize) {
try {
// initialSize的使用在这里,poolingCount默认为0,故会初始化initialSize个连接
PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
connections[poolingCount++] = holder;
} catch (SQLException ex) {
LOG.error("init datasource error, url: " + this.getUrl(), ex);
if (initExceptionThrow) {
connectError = ex;
break;
} else {
Thread.sleep(3000);
}
}
}
}
总结:initialSize比较简单,在DruidDataSource.init()方法调用完成后,默认会初始化initialSize个连接。所以建议在使用Spring创建DruidDataSource bean时,初始调用一下init方法,否则在第一次获取连接时会先执行init方法,拖慢第一次获取连接的速度。
3.2 maxActive的使用
// DruidDataSource.getConnectionDirect --> DruidDataSource.getConnectionInternal
// 直接获取连接时,要么直接从已创建好的连接池中获取,要么创建新的连接,这时maxActive就会派上用场
private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
if (closed) {
connectErrorCountUpdater.incrementAndGet(this);
throw new DataSourceClosedException("dataSource already closed at " + new Date(closeTimeMillis));
}
...
for (boolean createDirect = false;;) {
// createDirect什么时候会为true,看下面2的逻辑
if (createDirect) {
PhysicalConnectionInfo pyConnInfo = DruidDataSource.this.createPhysicalConnection();
holder = new DruidConnectionHolder(this, pyConnInfo);
holder.lastActiveTimeMillis = System.currentTimeMillis();
try {
// 此时maxActive排上用场,如果活跃的连接数大于maxActive,则discard
if (activeCount < maxActive) {
activeCount++;
holder.active = true;
if (activeCount > activePeak) {
activePeak = activeCount;
activePeakTime = System.currentTimeMillis();
}
break;
} else {
discard = true;
}
} finally {
lock.unlock();
}
// 超过maxActive的连接,则直接close掉
if (discard) {
JdbcUtils.close(pyConnInfo.getPhysicalConnection());
}
}
...
// 2
if (createScheduler != null
&& poolingCount == 0
&& activeCount < maxActive
&& creatingCountUpdater.get(this) == 0
&& createScheduler instanceof ScheduledThreadPoolExecutor) {
ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) createScheduler;
if (executor.getQueue().size() > 0) {
// 当poolingCount==0,也就是预创建连接池中的连接全部被占用
// 此时会执行上面的直接创建连接的逻辑
createDirect = true;
continue;
}
}
}
}
总结:当连接池中的连接全部被占用时,允许创建新的连接,但是总的连接数不能大于maxActive
3.3 minIdle的使用
// DruidDataSource.shrink
public void shrink(boolean checkTime, boolean keepAlive) {
final int checkCount = poolingCount - minIdle;
final long currentTimeMillis = System.currentTimeMillis();
for (int i = 0; i < poolingCount; ++i) {
DruidConnectionHolder connection = connections[i];
if (checkTime) {
if (phyTimeoutMillis > 0) {
long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
if (phyConnectTimeMillis > phyTimeoutMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
}
...
}
// 对minIdle之外的连接进行回收操作
int removeCount = evictCount + keepAliveCount;
if (removeCount > 0) {
System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
poolingCount -= removeCount;
}
keepAliveCheckCount += keepAliveCount;
if (keepAlive && poolingCount + activeCount < minIdle) {
needFill = true;
}
}
总结:当连接长时间不被使用时,会对其进行回收,当然,不是全部空闲连接都被回收,至少要保留minIdle个
4.连接池中连接有效性校验相关参数
4.1 获取连接时有效性检查
testWhileIdle:当程序请求连接时,连接池在分配连接时,是否先检查该连接是否有效
validationQuery:检查池中的连接是否仍有可用的SQL语句,druid会连接到数据库执行该SQL,如果正常返回,则表示连接可用,否则连接不可用
testOnBorrow:程序申请连接时,进行有效性检查
testOnReturn:程序返还连接时,进行连接有效性检查
1)testWhileIdle、testOnBorrow的使用
// DruidDataSource.getConnectionDirect
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
// testOnBorrow参数
if (testOnBorrow) {
// mysql下,默认使用MySqlValidConnectionChecker
// 实际上就是使用MysqlConnection.pingInternal方法进行ping操作
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
}else {
...
if (testWhileIdle) {
final DruidConnectionHolder holder = poolableConnection.holder;
long idleMillis = currentTimeMillis - lastActiveTimeMillis;
long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
if (timeBetweenEvictionRunsMillis <= 0) {
timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
}
// 如果当前连接空闲时间过长,超过我们指定的阈值后,则执行连接检查
if (idleMillis >= timeBetweenEvictionRunsMillis
|| idleMillis < 0) {
// 若连接已不可用,则discard,重新获取新的连接
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
discardConnection(poolableConnection.holder);
continue;
}
}
}
}
总结:当申请连接时,我们可以选择进行探活检查,对申请到的连接进行ping操作(mysql)。当然这个功能不建议开启,会对我们的业务有影响
2)validationQuery
// DruidDataSource.init
public void init() throws SQLException {
if (inited) {
return;
}
...
initFromSPIServiceLoader();
resolveDriver();
// 初始化检查会使用到validationQuery
initCheck();
...
}
protected void initCheck() throws SQLException {
if (JdbcUtils.ORACLE.equals(this.dbType)) {
isOracle = true;
...
// oracle检验
oracleValidationQueryCheck();
} else if (JdbcUtils.DB2.equals(dbType)) {
// db2校验
db2ValidationQueryCheck();
} else if (JdbcUtils.MYSQL.equals(this.dbType)
|| JdbcUtils.MYSQL_DRIVER_6.equals(this.dbType)) {
// mysql的话没有校验
isMySql = true;
}
...
}
3)testOnReturn
// DruidDataSource.cecycle
protected void recycle(DruidPooledConnection pooledConnection) throws SQLException {
...
// 回收连接,即将使用完毕的连接放回连接池
if (testOnReturn) {
// 同样进行可用性测试
boolean validate = testConnectionInternal(holder, physicalConnection);
if (!validate) {
JdbcUtils.close(physicalConnection);
destroyCountUpdater.incrementAndGet(this);
lock.lock();
try {
if (holder.active) {
activeCount--;
holder.active = false;
}
closeCount++;
} finally {
lock.unlock();
}
return;
}
}
}
总结:在连接获取或者连接回收时进行的探活操作,(mysql)下就是执行ping操作
4.2 获取连接超时检查
maxWait:从连接池中请求连接时,当超过maxWait后,则认为本次请求失败
// DruidDataSource.getConnection
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
init();
if (filters.size() > 0) {
FilterChainImpl filterChain = new FilterChainImpl(this);
return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
// 我们来分析直接获取连接的方式
return getConnectionDirect(maxWaitMillis);
}
}
// DruidDataSource.getConnectionDirect --> DruidDataSource.getConnectionInternal
private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
final long nanos = TimeUnit.MILLISECONDS.toNanos(maxWait);
...
// 当从连接池中获取连接时,可以选择等待maxWait,也可以选择一直等待下去
// 等待连接池的连接释放
if (maxWait > 0) {
holder = pollLast(nanos);
} else {
holder = takeLast();
}
}
总结:当我们使用连接池的连接时,若所有连接均被占用,则应用需要等待maxWait,若maxWait时间内没有等待连接释放,则报错
4.3 空闲连接检查
minEvictableIdleTimeMillis:当连接池中的某个连接的空闲时间达到N毫秒后,连接池在下次检查空闲连接时,将回收该连接
timeBetweenEvictionRunsMillis:检查空闲连接的频率,单位为毫秒
keepAlive:程序没有close连接且空闲时长超过minEvictableIdleTimeMillis,则会执行validationQuery指定的SQL,以保证该程序连接不会被kill,其范围不超过minIdle指定的连接个数
源码角度分析下这三个参数:
// DruidDataSource.init()方法执行时,也会默认创建CreatorThread和DestroyThread
public void init() throws SQLException {
...
createAndLogThread();
createAndStartCreatorThread();
// 创建销毁连接线程
createAndStartDestroyThread();
}
// DruidDataSource.createAndStartDestroyThread
protected void createAndStartDestroyThread() {
// 销毁任务
destroyTask = new DestroyTask();
if (destroyScheduler != null) {
long period = timeBetweenEvictionRunsMillis;
if (period <= 0) {
period = 1000;
}
// 定时器的检查周期即为timeBetweenEvictionRunsMillis
destroySchedulerFuture = destroyScheduler.scheduleAtFixedRate(destroyTask, period, period,
TimeUnit.MILLISECONDS);
initedLatch.countDown();
return;
}
String threadName = "Druid-ConnectionPool-Destroy-" + System.identityHashCode(this);
destroyConnectionThread = new DestroyConnectionThread(threadName);
destroyConnectionThread.start();
}
// DestroyTask.java
public class DestroyTask implements Runnable {
@Override
public void run() {
//
shrink(true, keepAlive);
if (isRemoveAbandoned()) {
removeAbandoned();
}
}
}
// DruidDataSource.shrink
public void shrink(boolean checkTime, boolean keepAlive) {
for (int i = 0; i < poolingCount; ++i) {
// 若当前连接空闲时间,超过minEvictableIdleTimeMillis,则回收该连接
long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
if (idleMillis >= minEvictableIdleTimeMillis) {
if (checkTime && i < checkCount) {
evictConnections[evictCount++] = connection;
continue;
} else if (idleMillis > maxEvictableIdleTimeMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
// 若空闲时间超过keepAliveBetweenTimeMillis,且当前支持keepAlive,则执行一次有效性检查操作
// 以更新连接的lastActiveTimeMillis,使其不被销毁
if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
keepAliveConnections[keepAliveCount++] = connection;
}
...
// 销毁长期空闲的连接(空闲时间大于minEvictableIdleTimeMillis)
if (evictCount > 0) {
for (int i = 0; i < evictCount; ++i) {
DruidConnectionHolder item = evictConnections[i];
Connection connection = item.getConnection();
JdbcUtils.close(connection);
destroyCountUpdater.incrementAndGet(this);
}
Arrays.fill(evictConnections, null);
}
// 对于空闲连接,执行SQL操作,更新其最新活跃时间,防止其被kill掉(空闲时间大于keepAliveBetweenTimeMillis)
for (int i = keepAliveCount - 1; i >= 0; --i) {
DruidConnectionHolder holer = keepAliveConnections[i];
Connection connection = holer.getConnection();
holer.incrementKeepAliveCheckCount();
boolean validate = false;
try {
// 探活检查
this.validateConnection(connection);
validate = true;
} catch (Throwable error) {
if (LOG.isDebugEnabled()) {
LOG.debug("keepAliveErr", error);
}
// skip
}
}
}
}
总结:对于长期没被使用的连接,则在keepAlive情况下,会默认发送validationQuery指定的SQL,以更新其最后活跃时间;否则,在超过最小空闲时间后,该连接会被close掉
4.4 连接使用回收检查
removeAbandoned:要求程序从连接池中获取到连接后,N秒后必须close,否则druid会强制回收该连接
removeAbandonedTimeout:设置druid强制回收连接的时限,当程序从池中获取到连接开始算起,超过此值后,druid强制回收该连接
// 在DestroyTask.java中,
public class DestroyTask implements Runnable {
@Override
public void run() {
// 检查空闲连接
shrink(true, keepAlive);
// 回收超时连接
if (isRemoveAbandoned()) {
removeAbandoned();
}
}
}
// DruidDataSource.removeAbandoned
public int removeAbandoned() {
...
Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator();
for (; iter.hasNext();) {
DruidPooledConnection pooledConnection = iter.next();
if (pooledConnection.isRunning()) {
continue;
}
long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);
// 连接的使用时间超过我们指定的阈值后,则直接close并丢弃
if (timeMillis >= removeAbandonedTimeoutMillis) {
iter.remove();
pooledConnection.setTraceEnable(false);
abandonedList.add(pooledConnection);
}
}
}
总结:这两个参数主要为了防止,应用在获取连接执行完相应的SQL处理后忘记归还连接。
有了这连个参数,就可以强制删除未归还的连接,后续会重新创建新的连接。
总结:
此时我们就可以回答下在文章开头,我们提出的几个问题
A:Druid是如何创建连接的,有哪些必备参数
Q:Druid创建连接本质上还是通过对应的Driver(根据不同的的数据库类型)的connect方法来创建连接的,具体可见之前的mysql-java篇博客;必备参数就是常用的minIdle、maxActive等
A:连接如何被回收
Q:长时间不用的连接会被回收掉。回收是DestroyTask做的工作,并保持minIdle最小活跃连接。
A:连接的有效性如何鉴定
Q:在获取连接、归还连接、空闲时检查,都可以对连接的有效性进行检查。针对mysql而言就是执行ping命令来检查有效性。