前言
本文将对Druid
数据库连接池的源码进行分析和学习,以了解Druid
数据库连接池的工作原理。Druid
数据库连接池的基本逻辑几乎全部在DruidDataSource
类中,所以本文主要是围绕DruidDataSource
的各项功能展开论述。
Druid
版本:1.2.11
正文
一. DruidDataSource初始化
DruidDataSource
初始化有两种方式,如下所示。
- 将
DruidDataSource
实例创建出来后,主动调用其init()
方法完成初始化; - 首次调用
DruidDataSource
的getConnection()
方法时,会调用到init()
方法完成初始化。
由于init()
方法过长,下面将分点介绍init()
方法完成的关键事情。
1. 双重检查inited状态
对inited状态进行Double Check,防止DruidDataSource
初始化两次。源码示意如下。
public void init() throws SQLException {
if (inited) {
return;
}
......
final ReentrantLock lock = this.lock;
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
throw new SQLException("interrupt", e);
}
boolean init = false;
try {
if (inited) {
return;
}
......
} catch (SQLException e) {
......
} catch (InterruptedException e) {
......
} catch (RuntimeException e) {
......
} catch (Error e) {
......
} finally {
inited = true;
lock.unlock();
......
}
}
2. 判断数据库类型
根据jdbcUrl得到数据库类型dbTypeName。源码如下所示。
if (this.dbTypeName == null || this.dbTypeName.length() == 0) {
this.dbTypeName = JdbcUtils.getDbType(jdbcUrl, null);
}
3. 参数校验
对一些关键参数进行校验。源码如下所示。
// 连接池最大连接数量不能小于等于0
if (maxActive <= 0) {
throw new IllegalArgumentException("illegal maxActive " + maxActive);
}
// 连接池最大连接数量不能小于最小连接数量
if (maxActive < minIdle) {
throw new IllegalArgumentException("illegal maxActive " + maxActive);
}
// 连接池初始连接数量不能大于最大连接数量
if (getInitialSize() > maxActive) {
throw new IllegalArgumentException("illegal initialSize " + this.initialSize + ", maxActive " + maxActive);
}
// 不允许同时开启基于日志手段记录连接池状态和全局状态监控
if (timeBetweenLogStatsMillis > 0 && useGlobalDataSourceStat) {
throw new IllegalArgumentException("timeBetweenLogStatsMillis not support useGlobalDataSourceStat=true");
}
// 连接最大空闲时间不能小于连接最小空闲时间
if (maxEvictableIdleTimeMillis < minEvictableIdleTimeMillis) {
throw new SQLException("maxEvictableIdleTimeMillis must be grater than minEvictableIdleTimeMillis");
}
// 不允许开启了保活机制但保活间隔时间小于等于回收检查时间间隔
if (keepAlive && keepAliveBetweenTimeMillis <= timeBetweenEvictionRunsMillis) {
throw new SQLException("keepAliveBetweenTimeMillis must be grater than timeBetweenEvictionRunsMillis");
}
4. SPI机制加载过滤器
调用到DruidDataSource#initFromSPIServiceLoader
方法,基于SPI机制加载过滤器Filter
。源码如下所示。
private void initFromSPIServiceLoader() {
if (loadSpifilterSkip) {
return;
}
if (autoFilters == null) {
List filters = new ArrayList();
// 基于ServiceLoader加载Filter
ServiceLoader autoFilterLoader = ServiceLoader.load(Filter.class);
// 遍历加载的每一个Filter,根据@AutoLoad注解的属性判断是否加载该Filter
for (Filter filter : autoFilterLoader) {
AutoLoad autoLoad = filter.getClass().getAnnotation(AutoLoad.class);
if (autoLoad != null && autoLoad.value()) {
filters.add(filter);
}
}
autoFilters = filters;
}
// 将每个需要加载的Filter添加到filters字段中,并去重
for (Filter filter : autoFilters) {
if (LOG.isInfoEnabled()) {
LOG.info("load filter from spi :" + filter.getClass().getName());
}
addFilter(filter);
}
}
5. 加载驱动
调用DruidDataSource#resolveDriver
方法,根据配置的驱动名称加载数据库驱动。源码如下所示。
protected void resolveDriver() throws SQLException {
if (this.driver == null) {
// 若没有配置驱动名则尝试从jdbcUrl中获取
if (this.driverClass == null || this.driverClass.isEmpty()) {
this.driverClass = JdbcUtils.getDriverClassName(this.jdbcUrl);
}
// Mock驱动相关
if (MockDriver.class.getName().equals(driverClass)) {
driver = MockDriver.instance;
} else if ("com.alibaba.druid.support.clickhouse.BalancedClickhouseDriver".equals(driverClass)) {
// ClickHouse相关
Properties info = new Properties();
info.put("user", username);
info.put("password", password);
info.putAll(connectProperties);
driver = new BalancedClickhouseDriver(jdbcUrl, info);
} else {
if (jdbcUrl == null && (driverClass == null || driverClass.length() == 0)) {
throw new SQLException("url not set");
}
// 加载驱动
driver = JdbcUtils.createDriver(driverClassLoader, driverClass);
}
} else {
if (this.driverClass == null) {
this.driverClass = driver.getClass().getName();
}
}
}
6. 初始化连接有效性校验器
调用DruidDataSource#initValidConnectionChecker
方法,初始化ValidConnectionChecker
,用于校验某个连接是否可用。源码如下所示。
private void initValidConnectionChecker() {
if (this.validConnectionChecker != null) {
return;
}
String realDriverClassName = driver.getClass().getName();
// 不同的数据库初始化不同的ValidConnectionChecker
if (JdbcUtils.isMySqlDriver(realDriverClassName)) {
// MySQL数据库还支持使用ping的方式来校验连接活性,这比执行一条简单查询语句来判活更高效
// 由usePingMethod参数决定是否开启
this.validConnectionChecker = new MySqlValidConnectionChecker(usePingMethod);
} else if (realDriverClassName.equals(JdbcConstants.ORACLE_DRIVER)
|| realDriverClassName.equals(JdbcConstants.ORACLE_DRIVER2)) {
this.validConnectionChecker = new OracleValidConnectionChecker();
} else if (realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER)
|| realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER_SQLJDBC4)
|| realDriverClassName.equals(JdbcConstants.SQL_SERVER_DRIVER_JTDS)) {
this.validConnectionChecker = new MSSQLValidConnectionChecker();
} else if (realDriverClassName.equals(JdbcConstants.POSTGRESQL_DRIVER)
|| realDriverClassName.equals(JdbcConstants.ENTERPRISEDB_DRIVER)
|| realDriverClassName.equals(JdbcConstants.POLARDB_DRIVER)) {
this.validConnectionChecker = new PGValidConnectionChecker();
} else if (realDriverClassName.equals(JdbcConstants.OCEANBASE_DRIVER)
|| (realDriverClassName.equals(JdbcConstants.OCEANBASE_DRIVER2))) {
DbType dbType = DbType.of(this.dbTypeName);
this.validConnectionChecker = new OceanBaseValidConnectionChecker(dbType);
}
}
7. 初始化全局状态统计器
如果useGlobalDataSourceStat设置为true,则初始化全局状态统计器,用于统计和分析数据库连接池的性能数据。源码片段如下所示。
if (isUseGlobalDataSourceStat()) {
dataSourceStat = JdbcDataSourceStat.getGlobal();
if (dataSourceStat == null) {
dataSourceStat = new JdbcDataSourceStat("Global", "Global", this.dbTypeName);
JdbcDataSourceStat.setGlobal(dataSourceStat);
}
if (dataSourceStat.getDbType() == null) {
dataSourceStat.setDbType(this.dbTypeName);
}
} else {
dataSourceStat = new JdbcDataSourceStat(this.name, this.jdbcUrl, this.dbTypeName, this.connectProperties);
}
8. 初始化连接池数组并预热
创建三个连接池数组,分别是connections(用于存放能获取的连接对象),evictConnections(用于存放需要丢弃的连接对象)和keepAliveConnections(用于存放需要保活的连接对象)。连接池的预热有两种,如果配置了asyncInit为true,且异步线程池不为空,则执行异步连接池预热,反之执行同步连接池预热。
// 用于存放能获取的连接对象,真正意义上的连接池
// 已经被获取的连接不在其中
connections = new DruidConnectionHolder[maxActive];
// 用于存放需要被关闭丢弃的连接
evictConnections = new DruidConnectionHolder[maxActive];
// 用于存放需要保活的连接
keepAliveConnections = new DruidConnectionHolder[maxActive];
SQLException connectError = null;
// 有线程池且异步初始化配置为true,则异步预热
if (createScheduler != null && asyncInit) {
for (int i = 0; i < initialSize; ++i) {
submitCreateTask(true);
}
} else if (!asyncInit) {
// 同步预热,预热连接数由initialSize配置
while (poolingCount < initialSize) {
try {
PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
// 对DruidDataSource和Connection做了一层封装
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);
}
}
}
if (poolingCount > 0) {
poolingPeak = poolingCount;
poolingPeakTime = System.currentTimeMillis();
}
}
9. 创建日志记录线程并启动
调用DruidDataSource#createAndLogThread
方法创建通过打印日志来记录连接池状态的线程。createAndLogThread()
方法如下所示。
private void createAndLogThread() {
// timeBetweenLogStatsMillis小于等于0表示不开启打印日志记录连接池状态的功能
if (this.timeBetweenLogStatsMillis <= 0) {
return;
}
String threadName = "Druid-ConnectionPool-Log-" + System.identityHashCode(this);
// 创建线程
logStatsThread = new LogStatsThread(threadName);
// 启动线程
logStatsThread.start();
this.resetStatEnable = false;
}
createAndLogThread()
方法会创建LogStatsThread
并启动,即会调用到LogStatsThread
的run()
方法。LogStatsThread
线程的run()
方法如下所示。
public void run() {
try {
for (; ; ) {
try {
// 每间隔timeBetweenLogStatsMillis就打印一次连接池状态
logStats();
} catch (Exception e) {
LOG.error("logStats error", e);
}
Thread.sleep(timeBetweenLogStatsMillis);
}
} catch (InterruptedException e) {
}
}
上述run()
方法中会每间隔timeBetweenLogStatsMillis的时间就调用一次logStats()
方法来打印连接池状态。logStats()
方法如下所示。
public void logStats() {
final DruidDataSourceStatLogger statLogger = this.statLogger;
if (statLogger == null) {
return;
}
// 拿到各种连接池的状态
DruidDataSourceStatValue statValue = getStatValueAndReset();
// 打印
statLogger.log(statValue);
}
在logStats()
方法中会先调用getStatValueAndReset()
方法来拿到各种连接池的状态,然后调用DruidDataSourceStatLogger
完成打印。最后看一眼getStatValueAndReset()
方法里面拿哪些连接池状态,getStatValueAndReset()
方法代码片段如下所示。
public DruidDataSourceStatValue getStatValueAndReset() {
DruidDataSourceStatValue value = new DruidDataSourceStatValue();
lock.lock();
try {
value.setPoolingCount(this.poolingCount);
value.setPoolingPeak(this.poolingPeak);
value.setPoolingPeakTime(this.poolingPeakTime);
value.setActiveCount(this.activeCount);
value.setActivePeak(this.activePeak);
value.setActivePeakTime(this.activePeakTime);
value.setConnectCount(this.connectCount);
value.setCloseCount(this.closeCount);
value.setWaitThreadCount(lock.getWaitQueueLength(notEmpty));
value.setNotEmptyWaitCount(this.notEmptyWaitCount);
value.setNotEmptyWaitNanos(this.notEmptyWaitNanos);
value.setKeepAliveCheckCount(this.keepAliveCheckCount);
// 重置参数
this.poolingPeak = 0;
this.poolingPeakTime = 0;
this.activePeak = 0;
this.activePeakTime = 0;
this.connectCount = 0;
this.closeCount = 0;
this.keepAliveCheckCount = 0;
this.notEmptyWaitCount = 0;
this.notEmptyWaitNanos = 0;
} finally {
lock.unlock();
}
value.setName(this.getName());
value.setDbType(this.dbTypeName);
value.setDriverClassName(this.getDriverClassName());
......
value.setSqlSkipCount(this.getDataSourceStat().getSkipSqlCountAndReset());
value.setSqlList(this.getDataSourceStat().getSqlStatMapAndReset());
return value;
}
10. 创建创建连接的线程并启动
调用DruidDataSource#createAndStartCreatorThread
方法来创建创建连接的线程CreateConnectionThread
并启动。createAndStartCreatorThread()
方法如下所示。
protected void createAndStartCreatorThread() {
// 只有异步创建连接的线程池为空时,才创建CreateConnectionThread
if (createScheduler == null) {
String threadName = "Druid-ConnectionPool-Create-" + System.identityHashCode(this);
createConnectionThread = new CreateConnectionThread(threadName);
// 启动线程
createConnectionThread.start();
return;
}
initedLatch.countDown();
}
CreateConnectionThread
只有在异步创建连接的线程池createScheduler为空时,才会被创建出来,并且在CreateConnectionThread
的run()
方法一开始,就会调用initedLatch的countDown()
方法,其中initedLatch是一个初始值为2的CountDownLatch
对象,另外一次countDown()
调用在DestroyConnectionThread
的run()
方法中,目的就是init()
方法执行完以前,创建连接的线程和销毁连接的线程一定要创建出来并启动完毕。
createAndLogThread();
// 在内部会调用到initedLatch.countDown()
createAndStartCreatorThread();
// 在内部最终会调用initedLatch.countDown()
createAndStartDestroyThread();
initedLatch.await();
11. 创建销毁连接的线程并启动
调用DruidDataSource#createAndStartDestroyThread
方法来创建销毁连接的线程DestroyConnectionThread
并启动。createAndStartDestroyThread()
方法如下所示。
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);
initedLatch.countDown();
return;
}
// 如果销毁连接的线程池为空,则创建销毁连接的线程
String threadName = "Druid-ConnectionPool-Destroy-" + System.identityHashCode(this);
destroyConnectionThread = new DestroyConnectionThread(threadName);
// 启动线程
destroyConnectionThread.start();
}
createAndStartDestroyThread()
方法中会先判断销毁连接的线程池是否存在,如果存在,则不再创建DestroyConnectionThread
,而是会让销毁连接的线程池来执行销毁任务,如果不存在,则创建DestroyConnectionThread
并启动,此时initedLatch的countDown()
调用是在DestroyConnectionThread
的run()
方法中。DestroyConnectionThread#run
方法源码如下所示。
public void run() {
// run()方法只要执行了,就调用initedLatch#countDown
initedLatch.countDown();
for (; ; ) {
// 每间隔timeBetweenEvictionRunsMillis执行一次DestroyTask的run()方法
try {
if (closed || closing) {
break;
}
if (timeBetweenEvictionRunsMillis > 0) {
Thread.sleep(timeBetweenEvictionRunsMillis);
} else {
Thread.sleep(1000);
}
if (Thread.interrupted()) {
break;
}
// 执行DestroyTask的run()方法来销毁需要销毁的线程
destroyTask.run();
} catch (InterruptedException e) {
break;
}
}
}
DestroyConnectionThread#run
方法只要被调用到,那么就会调用initedLatch的countDown()
方法,此时阻塞在init()
方法中的initedLatch.await()
方法上的线程就会被唤醒并继续往下执行。
二. DruidDataSource连接创建
DruidDataSource
连接的创建由CreateConnectionThread
线程完成,其run()
方法如下所示。
public void run() {
initedLatch.countDown();
long lastDiscardCount = 0;
int errorCount = 0;
for (; ; ) {
try {
lock.lockInterruptibly();
} catch (InterruptedException e2) {
break;
}
long discardCount = DruidDataSource.this.discardCount;
boolean discardChanged = discardCount - lastDiscardCount > 0;
lastDiscardCount = discardCount;
try {
// emptyWait为true表示生产连接线程需要等待,无需生产连接
boolean emptyWait = true;
// 发生了创建错误,且池中已无连接,且丢弃连接的统计没有改变
// 此时生产连接线程需要生产连接
if (createError != null
&& poolingCount == 0
&& !discardChanged) {
emptyWait = false;
}
if (emptyWait
&& asyncInit && createCount < initialSize) {
emptyWait = false;
}
if (emptyWait) {
// 池中已有连接数大于等于正在等待连接的应用线程数
// 且当前是非keepAlive场景
// 且当前是非连续失败
// 此时生产连接的线程在empty上等待
// keepAlive && activeCount + poolingCount < minIdle时会在shrink()方法中触发emptySingal()来添加连接
// isFailContinuous()返回true表示连续失败,即多次(默认2次)创建物理连接失败
if (poolingCount >= notEmptyWaitThreadCount
&& (!(keepAlive && activeCount + poolingCount < minIdle))
&& !isFailContinuous()
) {
empty.await();
}
// 防止创建超过maxActive数量的连接
if (activeCount + poolingCount >= maxActive) {
empty.await();
continue;
}
}
} catch (InterruptedException e) {
......
} finally {
lock.unlock();
}
PhysicalConnectionInfo connection = null;
try {
connection = createPhysicalConnection();
} catch (SQLException e) {
LOG.error("create connection SQLException, url: " + jdbcUrl + ", errorCode " + e.getErrorCode()
+ ", state " + e.getSQLState(), e);
errorCount++;
if (errorCount > connectionErrorRetryAttempts && timeBetweenConnectErrorMillis > 0) {
// 多次创建失败
setFailContinuous(true);
// 如果配置了快速失败,就唤醒所有在notEmpty上等待的应用线程
if (failFast) {
lock.lock();
try {
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
if (breakAfterAcquireFailure) {
break;
}
try {
Thread.sleep(timeBetweenConnectErrorMillis);
} catch (InterruptedException interruptEx) {
break;
}
}
} catch (RuntimeException e) {
LOG.error("create connection RuntimeException", e);
setFailContinuous(true);
continue;
} catch (Error e) {
LOG.error("create connection Error", e);
setFailContinuous(true);
break;
}
if (connection == null) {
continue;
}
// 把连接添加到连接池
boolean result = put(connection);
if (!result) {
JdbcUtils.close(connection.getPhysicalConnection());
LOG.info("put physical connection to pool failed.");
}
errorCount = 0;
if (closing || closed) {
break;
}
}
}
CreateConnectionThread
的run()
方法整体就是在一个死循环中不断的等待,被唤醒,然后创建线程。当一个物理连接被创建出来后,会调用DruidDataSource#put
方法将其放到连接池connections中,put()
方法源码如下所示。
protected boolean put(PhysicalConnectionInfo physicalConnectionInfo) {
DruidConnectionHolder holder = null;
try {
holder = new DruidConnectionHolder(DruidDataSource.this, physicalConnectionInfo);
} catch (SQLException ex) {
......
return false;
}
return put(holder, physicalConnectionInfo.createTaskId, false);
}
private boolean put(DruidConnectionHolder holder, long createTaskId, boolean checkExists) {
// 涉及到连接池中连接数量改变的操作,都需要加锁
lock.lock();
try {
if (this.closing || this.closed) {
return false;
}
// 池中已有连接数已经大于等于最大连接数,则不再把连接加到连接池并直接返回false
if (poolingCount >= maxActive) {
if (createScheduler != null) {
clearCreateTask(createTaskId);
}
return false;
}
// 检查重复添加
if (checkExists) {
for (int i = 0; i < poolingCount; i++) {
if (connections[i] == holder) {
return false;
}
}
}
// 连接放入连接池
connections[poolingCount] = holder;
// poolingCount++
incrementPoolingCount();
if (poolingCount > poolingPeak) {
poolingPeak = poolingCount;
poolingPeakTime = System.currentTimeMillis();
}
// 唤醒在notEmpty上等待连接的应用线程
notEmpty.signal();
notEmptySignalCount++;
if (createScheduler != null) {
clearCreateTask(createTaskId);
if (poolingCount + createTaskCount < notEmptyWaitThreadCount
&& activeCount + poolingCount + createTaskCount < maxActive) {
emptySignal();
}
}
} finally {
lock.unlock();
}
return true;
}
put()
方法会先将物理连接从PhysicalConnectionInfo
中获取出来并封装成一个DruidConnectionHolder
,DruidConnectionHolder
就是Druid
连接池中的连接。新添加的连接会存放在连接池数组connections的poolingCount位置,然后poolingCount会加1,也就是poolingCount代表着连接池中可以获取的连接的数量。
三. DruidDataSource连接销毁
DruidDataSource
连接的创建由DestroyConnectionThread
线程完成,其run()
方法如下所示。
public void run() {
// run()方法只要执行了,就调用initedLatch#countDown
initedLatch.countDown();
for (; ; ) {
// 每间隔timeBetweenEvictionRunsMillis执行一次DestroyTask的run()方法
try {
if (closed || closing) {
break;
}
if (timeBetweenEvictionRunsMillis > 0) {
Thread.sleep(timeBetweenEvictionRunsMillis);
} else {
Thread.sleep(1000);
}
if (Thread.interrupted()) {
break;
}
// 执行DestroyTask的run()方法来销毁需要销毁的连接
destroyTask.run();
} catch (InterruptedException e) {
break;
}
}
}
DestroyConnectionThread
的run()
方法就是在一个死循环中每间隔timeBetweenEvictionRunsMillis的时间就执行一次DestroyTask
的run()
方法。DestroyTask#run
方法实现如下所示。
public void run() {
// 根据一系列条件判断并销毁连接
shrink(true, keepAlive);
// RemoveAbandoned机制
if (isRemoveAbandoned()) {
removeAbandoned();
}
}
在DestroyTask#run
方法中会调用DruidDataSource#shrink
方法来根据设定的条件来判断出需要销毁和保活的连接。DruidDataSource#shrink
方法如下所示。
// checkTime参数表示在将一个连接进行销毁前,是否需要判断一下空闲时间
public void shrink(boolean checkTime, boolean keepAlive) {
// 加锁
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
return;
}
// needFill = keepAlive && poolingCount + activeCount < minIdle
// needFill为true时,会调用empty.signal()唤醒生产连接的线程来生产连接
boolean needFill = false;
// evictCount记录需要销毁的连接数
// keepAliveCount记录需要保活的连接数
int evictCount = 0;
int keepAliveCount = 0;
int fatalErrorIncrement = fatalErrorCount - fatalErrorCountLastShrink;
fatalErrorCountLastShrink = fatalErrorCount;
try {
if (!inited) {
return;
}
// checkCount = 池中已有连接数 - 最小空闲连接数
// 正常情况下,最多能够将前checkCount个连接进行销毁
final int checkCount = poolingCount - minIdle;
final long currentTimeMillis = System.currentTimeMillis();
// 正常情况下,需要遍历池中所有连接
// 从前往后遍历,i为数组索引
for (int i = 0; i < poolingCount; ++i) {
DruidConnectionHolder connection = connections[i];
// 如果发生了致命错误(onFatalError == true)且致命错误发生时间(lastFatalErrorTimeMillis)在连接建立时间之后
// 把连接加入到保活连接数组中
if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) {
keepAliveConnections[keepAliveCount++] = connection;
continue;
}
if (checkTime) {
// phyTimeoutMillis表示连接的物理存活超时时间,默认值是-1
if (phyTimeoutMillis > 0) {
// phyConnectTimeMillis表示连接的物理存活时间
long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
// 连接的物理存活时间大于phyTimeoutMillis,则将这个连接放入evictConnections数组
if (phyConnectTimeMillis > phyTimeoutMillis) {
evictConnections[evictCount++] = connection;
continue;
}
}
// idleMillis表示连接的空闲时间
long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
// minEvictableIdleTimeMillis表示连接允许的最小空闲时间,默认是30分钟
// keepAliveBetweenTimeMillis表示保活间隔时间,默认是2分钟
// 如果连接的空闲时间小于minEvictableIdleTimeMillis且还小于keepAliveBetweenTimeMillis
// 则connections数组中当前连接之后的连接都会满足空闲时间小于minEvictableIdleTimeMillis且还小于keepAliveBetweenTimeMillis
// 此时跳出遍历,不再检查其余的连接
if (idleMillis < minEvictableIdleTimeMillis
&& idleMillis < keepAliveBetweenTimeMillis
) {
break;
}
// 连接的空闲时间大于等于允许的最小空闲时间
if (idleMillis >= minEvictableIdleTimeMillis) {
if (checkTime && i < checkCount) {
// i < checkCount这个条件的理解如下:
// 每次shrink()方法执行时,connections数组中只有索引0到checkCount-1的连接才允许被销毁
// 这样才能保证销毁完连接后,connections数组中至少还有minIdle个连接
evictConnections[evictCount++] = connection;
continue;
} else if (idleMillis > maxEvictableIdleTimeMillis) {
// 如果空闲时间过久,已经大于了允许的最大空闲时间(默认7小时)
// 那么无论如何都要销毁这个连接
evictConnections[evictCount++] = connection;
continue;
}
}
// 如果开启了保活机制,且连接空闲时间大于等于了保活间隔时间
// 此时将连接加入到保活连接数组中
if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
keepAliveConnections[keepAliveCount++] = connection;
}
} else {
// checkTime为false,那么前checkCount个连接直接进行销毁,不再判断这些连接的空闲时间是否超过阈值
if (i < checkCount) {
evictConnections[evictCount++] = connection;
} else {
break;
}
}
}
// removeCount = 销毁连接数 + 保活连接数
// removeCount表示本次从connections数组中拿掉的连接数
// 注:一定是从前往后拿,正常情况下最后minIdle个连接是安全的
int removeCount = evictCount + keepAliveCount;
if (removeCount > 0) {
// [0, 1, 2, 3, 4, null, null, null] -> [3, 4, 2, 3, 4, null, null, null]
System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
// [3, 4, 2, 3, 4, null, null, null] -> [3, 4, null, null, null, null, null, null, null]
Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
// 更新池中连接数
poolingCount -= removeCount;
}
keepAliveCheckCount += keepAliveCount;
// 如果池中连接数加上活跃连接数(借出去的连接)小于最小空闲连接数
// 则将needFill设为true,后续需要唤醒生产连接的线程来生产连接
if (keepAlive && poolingCount + activeCount < minIdle) {
needFill = true;
}
} finally {
lock.unlock();
}
if (evictCount > 0) {
// 遍历evictConnections数组,销毁其中的连接
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);
}
if (keepAliveCount > 0) {
// 遍历keepAliveConnections数组,对其中的连接做可用性校验
// 校验通过连接就放入connections数组,没通过连接就销毁
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);
}
}
boolean discard = !validate;
if (validate) {
holer.lastKeepTimeMillis = System.currentTimeMillis();
boolean putOk = put(holer, 0L, true);
if (!putOk) {
discard = true;
}
}
if (discard) {
try {
connection.close();
} catch (Exception e) {
}
lock.lock();
try {
discardCount++;
if (activeCount + poolingCount <= minIdle) {
emptySignal();
}
} finally {
lock.unlock();
}
}
}
this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
Arrays.fill(keepAliveConnections, null);
}
// 如果needFill为true则唤醒生产连接的线程来生产连接
if (needFill) {
lock.lock();
try {
// 计算需要生产连接的个数
int fillCount = minIdle - (activeCount + poolingCount + createTaskCount);
for (int i = 0; i < fillCount; ++i) {
emptySignal();
}
} finally {
lock.unlock();
}
} else if (onFatalError || fatalErrorIncrement > 0) {
lock.lock();
try {
emptySignal();
} finally {
lock.unlock();
}
}
}
在DruidDataSource#shrink
方法中,核心逻辑是遍历connections数组中的连接,并判断这些连接是需要销毁还是需要保活。通常情况下,connections数组中的前checkCount(checkCount = poolingCount - minIdle)个连接是“危险”的,因为这些连接只要满足了:空闲时间 >= minEvictableIdleTimeMillis(允许的最小空闲时间),那么就需要被销毁,而connections数组中的最后minIdle个连接是“相对安全”的,因为这些连接只有在满足:空闲时间 > maxEvictableIdleTimeMillis(允许的最大空闲时间)时,才会被销毁。这么判断的原因,主要就是需要让连接池里能够保证至少有minIdle个空闲连接可以让应用线程获取。
当确定好了需要销毁和需要保活的连接后,此时会先将connections数组清理,只保留安全的连接,这个过程示意图如下。
最后,会遍历evictConnections数组,销毁数组中的连接,遍历keepAliveConnections数组,对其中的每个连接做可用性校验,如果校验可用,那么就重新放回connections数组,否则销毁。
四. DruidDataSource连接获取
DruidDataSource
获取连接的入口方法是DruidDataSource#getConnection
方法,实现如下。
public DruidPooledConnection getConnection() throws SQLException {
// maxWait表示获取连接时最大等待时间,单位毫秒,默认值为-1
return getConnection(maxWait);
}
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#getConnection
方法会调用到DruidDataSource#getConnectionDirect
方法来获取连接,实现如下所示。
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
int notFullTimeoutRetryCnt = 0;
for (; ; ) {
DruidPooledConnection poolableConnection;
try {
// 从连接池拿到连接
poolableConnection = getConnectionInternal(maxWaitMillis);
} catch (GetConnectionTimeoutException ex) {
// 拿连接时有异常,可以重试
// 重试次数由notFullTimeoutRetryCount指定
if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
notFullTimeoutRetryCnt++;
if (LOG.isWarnEnabled()) {
LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
}
continue;
}
throw ex;
}
// 如果配置了testOnBorrow = true,那么每次拿到连接后,都需要校验这个连接的有效性
if (testOnBorrow) {
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
// 如果连接不可用,则销毁连接,然后重新从池中获取
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
} else {
if (poolableConnection.conn.isClosed()) {
discardConnection(poolableConnection.holder);
continue;
}
// 如果配置testOnBorrow = fasle但testWhileIdle = true
// 则判断连接空闲时间是否大于等于timeBetweenEvictionRunsMillis
// 如果是,则校验连接的有效性
if (testWhileIdle) {
final DruidConnectionHolder holder = poolableConnection.holder;
long currentTimeMillis = System.currentTimeMillis();
// lastActiveTimeMillis是连接最近一次活跃时间
// 新建连接,归还连接到连接池,都会更新这个时间
long lastActiveTimeMillis = holder.lastActiveTimeMillis;
// lastExecTimeMillis是连接最近一次执行时间
// 新建连接,设置连接的事务是否自动提交,记录SQL到事务信息中,都会更新这个时间
long lastExecTimeMillis = holder.lastExecTimeMillis;
// lastKeepTimeMillis是连接最近一次保活时间
// 在连接被保活并放回连接池时,会更新这个时间
long lastKeepTimeMillis = holder.lastKeepTimeMillis;
// 如果配置checkExecuteTime为true,则最近活跃时间取值为最近执行时间
if (checkExecuteTime
&& lastExecTimeMillis != lastActiveTimeMillis) {
lastActiveTimeMillis = lastExecTimeMillis;
}
// 如果连接最近一次做的操作是保活,那么最近活跃时间取值为最近保活时间
if (lastKeepTimeMillis > lastActiveTimeMillis) {
lastActiveTimeMillis = lastKeepTimeMillis;
}
// 计算空闲时间
long idleMillis = currentTimeMillis - lastActiveTimeMillis;
// testWhileIdle为true时的判断时间间隔
long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
if (timeBetweenEvictionRunsMillis <= 0) {
// timeBetweenEvictionRunsMillis如果小于等于0,那么重置为60秒
timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
}
// 如果空闲时间大于等于timeBetweenEvictionRunsMillis,则执行连接的有效性校验
if (idleMillis >= timeBetweenEvictionRunsMillis
|| idleMillis < 0
) {
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
}
}
}
// 如果设置removeAbandoned为true
// 则将连接放到activeConnections活跃连接map中
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;
}
}
DruidDataSource#getConnectionDirect
方法中会先调用getConnectionInternal()
方法从连接池中拿连接,然后如果开启了testOnBorrow,则校验一下连接的有效性,如果无效则重新调用getConnectionInternal()
方法拿连接,直到拿到的连接通过校验。如果没有开启testOnBorrow但是开启了testWhileIdle,则会判断连接的空闲时间是否大于等于timeBetweenEvictionRunsMillis参数,如果满足则校验一下连接的有效性,若没有通过校验,那么需要重新调用getConnectionInternal()
方法拿连接,直到拿到的连接通过校验或者连接的空闲时间小于timeBetweenEvictionRunsMillis。
下面看一下实际从连接池拿连接的getConnectionInternal()
方法的实现,如下所示。
private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
......
final long nanos = TimeUnit.MILLISECONDS.toNanos(maxWait);
final int maxWaitThreadCount = this.maxWaitThreadCount;
DruidConnectionHolder holder;
// 在死循环中从连接池拿连接
// 一开始createDirect为false,表示先从池子中拿
for (boolean createDirect = false; ; ) {
if (createDirect) {
// createDirect为true表示直接创建连接
createStartNanosUpdater.set(this, System.nanoTime());
// creatingCount为0表示当前没有其它连接正在被创建
if (creatingCountUpdater.compareAndSet(this, 0, 1)) {
// 创建物理连接
PhysicalConnectionInfo pyConnInfo = DruidDataSource.this.createPhysicalConnection();
holder = new DruidConnectionHolder(this, pyConnInfo);
holder.lastActiveTimeMillis = System.currentTimeMillis();
creatingCountUpdater.decrementAndGet(this);
directCreateCountUpdater.incrementAndGet(this);
......
boolean discard;
lock.lock();
try {
// 如果当前正在使用的连接数未达到最大连接数
// 则当前正在使用的连接数加1
// 否则销毁刚刚创建出来的连接
if (activeCount < maxActive) {
activeCount++;
holder.active = true;
if (activeCount > activePeak) {
activePeak = activeCount;
activePeakTime = System.currentTimeMillis();
}
break;
} else {
discard = true;
}
} finally {
lock.unlock();
}
if (discard) {
JdbcUtils.close(pyConnInfo.getPhysicalConnection());
}
}
}
// 上锁
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
connectErrorCountUpdater.incrementAndGet(this);
throw new SQLException("interrupt", e);
}
try {
// maxWaitThreadCount表示允许的最大等待连接的应用线程数
// notEmptyWaitThreadCount表示正在等待连接的应用线程数
// 等待连接的应用线程数达到最大值时,抛出异常
if (maxWaitThreadCount > 0
&& notEmptyWaitThreadCount >= maxWaitThreadCount) {
connectErrorCountUpdater.incrementAndGet(this);
throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count "
+ lock.getQueueLength());
}
// 发生了致命错误,且设置了致命错误数最大值大于0,且正在使用的连接数大于等于致命错误数最大值
if (onFatalError
&& onFatalErrorMaxActive > 0
&& activeCount >= onFatalErrorMaxActive) {
// 拼接异常并抛出
......
throw new SQLException(
errorMsg.toString(), lastFatalError);
}
connectCount++;
// 如果配置的创建连接的线程池是一个定时线程池
// 且连接池已经没有可用连接,
// 且当前借出的连接数未达到允许的最大连接数
// 且当前没有其它线程(应用线程,创建连接的线程,创建连接的线程池里的线程)在创建连接
// 此时将createDirect置为true,让当前应用线程直接创建连接
if (createScheduler != null
&& poolingCount == 0
&& activeCount < maxActive
&& creatingCountUpdater.get(this) == 0
&& createScheduler instanceof ScheduledThreadPoolExecutor) {
ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) createScheduler;
if (executor.getQueue().size() > 0) {
createDirect = true;
continue;
}
}
if (maxWait > 0) {
// 如果设置了等待连接的最大等待时间,则调用pollLast()方法来拿连接
// pollLast()方法执行时如果池中没有连接,则应用线程会在notEmpty上最多等待maxWait的时间
holder = pollLast(nanos);
} else {
// 调用takeLast()方法拿连接时,如果池中没有连接,则会在notEmpty上一直等待,直到池中有连接
holder = takeLast();
}
if (holder != null) {
if (holder.discard) {
continue;
}
// 正在使用的连接数加1
activeCount++;
holder.active = true;
if (activeCount > activePeak) {
activePeak = activeCount;
activePeakTime = System.currentTimeMillis();
}
}
} catch (InterruptedException e) {
connectErrorCountUpdater.incrementAndGet(this);
throw new SQLException(e.getMessage(), e);
} catch (SQLException e) {
connectErrorCountUpdater.incrementAndGet(this);
throw e;
} finally {
lock.unlock();
}
break;
}
// 如果拿到的连接为null,说明拿连接时等待超时了
// 此时抛出连接超时异常
if (holder == null) {
......
final Throwable createError;
try {
lock.lock();
......
createError = this.createError;
} finally {
lock.unlock();
}
......
if (createError != null) {
throw new GetConnectionTimeoutException(errorMessage, createError);
} else {
throw new GetConnectionTimeoutException(errorMessage);
}
}
holder.incrementUseCount();
DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder);
return poolalbeConnection;
}
getConnectionInternal()
方法中拿到连接的方式有三种,如下所示。
- 直接创建连接。需要满足配置的创建连接的线程池是一个定时线程池,且连接池已经没有可用连接,且当前借出的连接数未达到允许的最大连接数,且当前没有其它线程在创建连接;
- 从池中拿连接,并最多等待maxWait的时间。需要设置了maxWait;
- 从池中拿连接,并一直等待直到拿到连接。
下面最后看一下超时等待拿连接的DruidDataSource#pollLast
方法的实现。
private DruidConnectionHolder pollLast(long nanos) throws InterruptedException, SQLException {
long estimate = nanos;
for (; ; ) {
if (poolingCount == 0) {
// 如果池中已经没有连接,则唤醒在empty上等待的创建连接线程来创建连接
emptySignal();
if (failFast && isFailContinuous()) {
throw new DataSourceNotAvailableException(createError);
}
// 等待时间耗尽,返回null
if (estimate <= 0) {
waitNanosLocal.set(nanos - estimate);
return null;
}
// 应用线程即将在下面的notEmpty上等待
// 这里先把等待获取连接的应用线程数加1
notEmptyWaitThreadCount++;
if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) {
notEmptyWaitThreadPeak = notEmptyWaitThreadCount;
}
try {
long startEstimate = estimate;
// 应用线程在notEmpty上等待
// 有连接被创建或者被归还时,会唤醒在notEmpty上等待的应用线程
estimate = notEmpty.awaitNanos(estimate);
notEmptyWaitCount++;
notEmptyWaitNanos += (startEstimate - estimate);
if (!enable) {
connectErrorCountUpdater.incrementAndGet(this);
if (disableException != null) {
throw disableException;
}
throw new DataSourceDisableException();
}
} catch (InterruptedException ie) {
notEmpty.signal();
notEmptySignalCount++;
throw ie;
} finally {
notEmptyWaitThreadCount--;
}
if (poolingCount == 0) {
if (estimate > 0) {
// 若唤醒后池中还是没有连接,且此时等待时间还有剩余
// 则重新在notEmpty上等待
continue;
}
waitNanosLocal.set(nanos - estimate);
return null;
}
}
// poolingCount--
decrementPoolingCount();
// 从池中拿到连接
DruidConnectionHolder last = connections[poolingCount];
connections[poolingCount] = null;
long waitNanos = nanos - estimate;
last.setLastNotEmptyWaitNanos(waitNanos);
return last;
}
}
五. DruidDataSource连接归还
Druid
数据库连接池中,每一个物理连接都会被包装成DruidConnectionHolder
,在提供给应用线程前,还会将DruidConnectionHolder
包装成DruidPooledConnection
,类图如下所示。
应用线程中使用连接完毕后,会调用DruidPooledConnection
的close()
方法来归还连接,也就是将连接放回连接池。DruidPooledConnection#close
方法如下所示。
public void close() throws SQLException {
if (this.disable) {
return;
}
DruidConnectionHolder holder = this.holder;
if (holder == null) {
if (dupCloseLogEnable) {
LOG.error("dup close");
}
return;
}
DruidAbstractDataSource dataSource = holder.getDataSource();
// 判断归还连接的线程和获取连接的线程是否是同一个线程
boolean isSameThread = this.getOwnerThread() == Thread.currentThread();
// 如果不是同一个线程,则设置asyncCloseConnectionEnable为true
if (!isSameThread) {
dataSource.setAsyncCloseConnectionEnable(true);
}
// 如果开启了removeAbandoned机制
// 或者asyncCloseConnectionEnable为true
// 则调用syncClose()方法来归还连接
// syncClose()方法中会先加锁,然后调用recycle()方法来回收连接
if (dataSource.isAsyncCloseConnectionEnable()) {
syncClose();
return;
}
if (!CLOSING_UPDATER.compareAndSet(this, 0, 1)) {
return;
}
try {
for (ConnectionEventListener listener : holder.getConnectionEventListeners()) {
listener.connectionClosed(new ConnectionEvent(this));
}
List filters = dataSource.getProxyFilters();
if (filters.size() > 0) {
FilterChainImpl filterChain = new FilterChainImpl(dataSource);
filterChain.dataSource_recycle(this);
} else {
// 回收连接
recycle();
}
} finally {
CLOSING_UPDATER.set(this, 0);
}
this.disable = true;
}
在DruidPooledConnection#close
方法中,会先判断本次归还连接的线程和获取连接的线程是否是同一个线程,如果不是,则先加锁然后再调用recycle()
方法来回收连接,如果是则直接调用recycle()
方法来回收连接。当开启了removeAbandoned机制时,就可能会出现归还连接的线程和获取连接的线程不是同一个线程的情况,这是因为一旦开启了removeAbandoned机制,那么每一个被借出的连接都会被放到activeConnections活跃连接map中,并且在销毁连接的线程DestroyConnectionThread
中会每间隔timeBetweenEvictionRunsMillis的时间就遍历一次activeConnections活跃连接map,一旦有活跃连接被借出的时间大于了removeAbandonedTimeoutMillis,那么销毁连接的线程DestroyConnectionThread
就会主动去回收这个连接,以防止连接泄漏。
下面看一下DruidPooledConnection#recycle
方法的实现。
public void recycle() throws SQLException {
if (this.disable) {
return;
}
DruidConnectionHolder holder = this.holder;
if (holder == null) {
if (dupCloseLogEnable) {
LOG.error("dup close");
}
return;
}
if (!this.abandoned) {
DruidAbstractDataSource dataSource = holder.getDataSource();
// 调用DruidAbstractDataSource#recycle回收当前连接
dataSource.recycle(this);
}
this.holder = null;
conn = null;
transactionInfo = null;
closed = true;
}
在DruidPooledConnection#recycle
方法中会调用到DruidDataSource#recycle
方法来回收连接。DruidDataSource#recycle
方法实现如下所示。
protected void recycle(DruidPooledConnection pooledConnection) throws SQLException {
final DruidConnectionHolder holder = pooledConnection.holder;
......
final boolean isAutoCommit = holder.underlyingAutoCommit;
final boolean isReadOnly = holder.underlyingReadOnly;
final boolean testOnReturn = this.testOnReturn;
try {
// 如果是非自动提交且存在事务
// 则回滚事务
if ((!isAutoCommit) && (!isReadOnly)) {
pooledConnection.rollback();
}
// 重置连接信息(配置还原为默认值,关闭Statement,清除连接的Warnings等)
boolean isSameThread = pooledConnection.ownerThread == Thread.currentThread();
if (!isSameThread) {
final ReentrantLock lock = pooledConnection.lock;
lock.lock();
try {
holder.reset();
} finally {
lock.unlock();
}
} else {
holder.reset();
}
......
// 开启了testOnReturn机制,则校验连接有效性
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;
}
}
......
lock.lock();
try {
// 连接即将放回连接池,需要将active设置为false
if (holder.active) {
activeCount--;
holder.active = false;
}
closeCount++;
// 将连接放到connections数组的poolingCount位置
// 然后poolingCount加1
// 然后唤醒在notEmpty上等待连接的一个应用线程
result = putLast(holder, currentTimeMillis);
recycleCount++;
} finally {
lock.unlock();
}
if (!result) {
JdbcUtils.close(holder.conn);
LOG.info("connection recyle failed.");
}
} catch (Throwable e) {
......
}
}
DruidDataSource#recycle
方法中会先重置连接信息,即将连接的一些配置重置为默认值,然后关闭连接的Statement
和Warnings,如果开启了testOnReturn机制,则还需要校验一下连接的有效性,校验不通过则直接关闭物理连接,最后,将连接放回到connections数组的poolingCount位置,然后唤醒一个在notEmpty上等待连接的应用线程。
六. removeAbandoned机制
Druid
数据库连接池提供了removeAbandoned机制来防止连接泄漏。要开启removeAbandoned机制,需要设置如下参数。
参数 | 说明 |
---|---|
removeAbandoned | 发生连接泄漏时,是否需要回收泄漏的连接。默认为false,表示不回收。 |
removeAbandonedTimeoutMillis | 判断发生连接泄漏的超时时间。默认为300秒。 |
下面将对开启removeAbandoned机制后,如何回收发生了泄漏的连接进行说明。当应用线程从连接池获取到一个连接后,如果开启了removeAbandoned机制,那么会将这个连接放到activeConnections活跃连接map中,对应的方法为DruidDataSource#getConnectionDirect
,源码片段如下所示。
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
int notFullTimeoutRetryCnt = 0;
for (; ; ) {
DruidPooledConnection poolableConnection;
......
if (removeAbandoned) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
poolableConnection.connectStackTrace = stackTrace;
// 设置connectedTimeNano,用于后续判断连接借出时间是否大于removeAbandonedTimeoutMillis
poolableConnection.setConnectedTimeNano();
poolableConnection.traceEnable = true;
activeConnectionLock.lock();
try {
// 将从连接池获取到的连接放到activeConnections中
activeConnections.put(poolableConnection, PRESENT);
} finally {
activeConnectionLock.unlock();
}
}
if (!this.defaultAutoCommit) {
poolableConnection.setAutoCommit(false);
}
return poolableConnection;
}
}
又已知Druid
数据库连接池有一个销毁连接的线程会每间隔timeBetweenEvictionRunsMillis执行一次DestroyTask#run
方法来销毁连接,DestroyTask#run
方法如下所示。
public void run() {
shrink(true, keepAlive);
// 如果开启了removeAbandoned机制
// 则执行removeAbandoned()方法来检测发生了泄漏的连接并回收
if (isRemoveAbandoned()) {
removeAbandoned();
}
}
DestroyTask#run
方法的最后会判断是否开启了removeAbandoned机制,如果开启了则会执行DruidDataSource#removeAbandoned
方法来检测哪些连接发生了泄漏,并主动回收这些连接。DruidDataSource#removeAbandoned
方法如下所示。
public int removeAbandoned() {
int removeCount = 0;
long currrentNanos = System.nanoTime();
List abandonedList = new ArrayList();
activeConnectionLock.lock();
try {
Iterator iter = activeConnections.keySet().iterator();
for (; iter.hasNext(); ) {
DruidPooledConnection pooledConnection = iter.next();
// 运行中的连接不会被判定为发生了泄漏
if (pooledConnection.isRunning()) {
continue;
}
long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);
// 判断连接借出时间是否达到连接泄漏的超时时间
if (timeMillis >= removeAbandonedTimeoutMillis) {
// 将发生了泄漏的连接从activeConnections中移除
iter.remove();
pooledConnection.setTraceEnable(false);
// 将发生了泄露的连接添加到abandonedList集合中
abandonedList.add(pooledConnection);
}
}
} finally {
activeConnectionLock.unlock();
}
if (abandonedList.size() > 0) {
// 遍历abandonedList集合
// 主动调用每个发生了泄漏的DruidPooledConnection的close()方法来回收连接
for (DruidPooledConnection pooledConnection : abandonedList) {
final ReentrantLock lock = pooledConnection.lock;
lock.lock();
try {
if (pooledConnection.isDisable()) {
continue;
}
} finally {
lock.unlock();
}
JdbcUtils.close(pooledConnection);
pooledConnection.abandond();
removeAbandonedCount++;
removeCount++;
......
}
}
return removeCount;
}
DruidDataSource#removeAbandoned
方法中主要完成的事情就是将每个发生了泄漏的连接从activeConnections中移动到abandonedList中,然后遍历abandonedList中的每个连接并调用DruidPooledConnection#close
方法,最终完成泄漏连接的回收。
总结
Druid
数据库连接池中,应用线程向连接池获取连接时,如果池中没有连接,则应用线程会在notEmpty上等待,同时Druid
数据库连接池中有一个创建连接的线程,会持续的向连接池创建连接,如果连接池已满,则创建连接的线程会在empty上等待。
当有连接被生产,或者有连接被归还,会唤醒在notEmpty上等待的应用线程,同理有连接被销毁时,会唤醒在empty上等待的生产连接的线程。
Druid
数据库连接池中还有一个销毁连接的线程,会每间隔timeBetweenEvictionRunsMillis的时间执行一次DestroyTask
任务来销毁连接,这些被销毁的连接可以是存活时间达到最大值的连接,也可以是空闲时间达到指定值的连接。如果还开启了保活机制,那么空闲时间大于keepAliveBetweenTimeMillis的连接都会被校验一次有效性,校验不通过的连接会被销毁。
最后,Druid
数据库连接池提供了removeAbandoned机制来防止连接泄漏,当开启了removeAbandoned机制时,每一个被应用线程获取的连接都会被添加到activeConnections活跃连接map中,如果这个连接在应用线程中使用完毕后没有被关闭,那么Druid
数据库连接池会从activeConnections中将其识别出来并主动回收。