为什么要用连接池
作为一个后端开发,我们日常中要去请求各种各样的外部资源。例如去做数据库请求查询数据,去做http请求调用其他接口。所有这些,都是需要本地应用与其他服务器建立连接,才能获取资源的。
在没有连接池的情况下,我们怎么去连接呢?那当然只能是每请求一次,就去建立一次连接。可是在计算机的世界中,频繁建立连接是非常损耗资源的。所以当连接不用的时候,我们希望可以保持这个连接的状态,其他线程要用的时候,就把它取出来用。然后有了连接池的存在。接下来我们以阿里的Druid(德鲁伊)为例,来探究连接池的逻辑。我用的版本是这个:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
源码阅读
我们直接跟踪到 DruidDataSource 这个类。DruidConnectionHolder 数组存着所有池中的连接。
private volatile DruidConnectionHolder[] connections;
接下来我们看看getConnections方法。
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);
}
}
我们可以看到首先是执行一个init()方法,然后判断,如果有filter的话就通过filterChain获取连接,否则调用getConnectionDirect。继续往下跟。
我们跟进init方法来看看druid初始化了什么东西。
public void init() throws SQLException {
if (inited) {
return;
}
// bug fixed for dead lock, for issue #2980
DruidDriver.getInstance();
final ReentrantLock lock = this.lock;
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
throw new SQLException("interrupt", e);
}
boolean init = false;
try {、
// 上面这段代码,是单例模式经典的 DCL (Double Check Lock二次检查锁)
this.id = DruidDriver.createDataSourceId();
if (this.id > 1) {
long delta = (this.id - 1) * 100000;
this.connectionIdSeedUpdater.addAndGet(this, delta);
this.statementIdSeedUpdater.addAndGet(this, delta);
this.resultSetIdSeedUpdater.addAndGet(this, delta);
this.transactionIdSeedUpdater.addAndGet(this, delta);
}
if (this.jdbcUrl != null) {
// 这个 if 条件主要是给 druidDataSource 添加了filter,可以直接跳过
this.jdbcUrl = this.jdbcUrl.trim();
initFromWrapDriverUrl();
}
for (Filter filter : filters) {
filter.init(this);
}
if (this.dbTypeName == null || this.dbTypeName.length() == 0) {
// 根据jdbcUrl获取到连接的数据库类型,我连接的是mysql,所以返回的就是 "mysql"
this.dbTypeName = JdbcUtils.getDbType(jdbcUrl, null);
}
DbType dbType = DbType.of(this.dbTypeName);
// 根据dbType做些事情
// 接下来这一大段主要是校验关于连接池大小的一些参数是否合理,不合理则直接抛出错误
if (maxActive <= 0) {
throw new IllegalArgumentException("illegal maxActive " + maxActive);
}
// 处理通过配置文件配置的filter
initFromSPIServiceLoader();
// 获取配置的jdbcDriver
resolveDriver();
initCheck();
initExceptionSorter();
// 初始化ConnectionChecker 用于检查连接是否正常
initValidConnectionChecker();
validationQueryCheck();
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);
}
dataSourceStat.setResetStatEnable(this.resetStatEnable);
connections = new DruidConnectionHolder[maxActive];
evictConnections = new DruidConnectionHolder[maxActive];
keepAliveConnections = new DruidConnectionHolder[maxActive];
SQLException connectError = null;
// 这里开始创建连接了,可以看到druid初始化的时候会创建initialSize个连接。这个参数可以配置
// 可以看到druid 支持异步和同步两种执行创建任务的方式
if (createScheduler != null && asyncInit) {
for (int i = 0; i < initialSize; ++i) {
submitCreateTask(true);
}
} else if (!asyncInit) {
// init connections
while (poolingCount < initialSize) {
try {
// 这里就是真正创建连接的地方了,可以看到创建后的连接被放入了 connections 这样一个数组中
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);
}
}
}
if (poolingCount > 0) {
poolingPeak = poolingCount;
poolingPeakTime = System.currentTimeMillis();
}
}
// 初始化 LogStatsThread,这个线程主要用来每隔timeBetweenLogStatsMillis记录一下dataSource的状态
// 包括已有连接数、出错连接数等等
createAndLogThread();
// 初始化CreateConnectionThread,这个线程主要用来创建连接。
// 这个线程用来给连接池补充连接
createAndStartCreatorThread();
// 初始化DestroyConnectionThread,这个线程无限循环,判断是否有需要丢弃的连接,进行丢弃
createAndStartDestroyThread();
initedLatch.await();
init = true;
initedTime = new Date();
registerMbean();
} catch (SQLException e) {
LOG.error("{dataSource-" + this.getID() + "} init error", e);
throw e;
} catch (InterruptedException e) {
throw new SQLException(e.getMessage(), e);
} catch (RuntimeException e){
LOG.error("{dataSource-" + this.getID() + "} init error", e);
throw e;
} catch (Error e){
LOG.error("{dataSource-" + this.getID() + "} init error", e);
throw e;
} finally {
inited = true;
lock.unlock();
}
}
可以看到,init 方法主要做了下面这些事情:
1、根据jdbcUrl判断连接的数据库类型,获取对应的JdbcDriver以及初始化ConnectionChecker。ConnectionChecker会被用于校验连接是否正常。
2、校验参数,主要包括initialSize、maxActive等参数是否合理。
3、初始化 initialSize 个数据库连接,有同步和异步两种方式。可以通过参数配置控制。
4、初始化三个线程4.1、LogStatsThread。主要用于周期性的打印dataSource状态。如活跃连接数等。
4.2、CreateConnectionThread。主要用于补充连接池中的连接。
4.3、DestroyConnectionThread。用于执行removeAbandoned逻辑。它会判断在activeConnections里的所有连接,如果连接上一次被取用的时间到当前时间的差值大于removeAbandonedTimeoutMillis,即连接空闲时间超过了一定时间。druid会调用JdbcUtils.close(pooledConnection)关闭当前连接,并调用abandond()连接的状态置为废弃状态。
下面这行代码即判断空闲时间的办法:
long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);
if (timeMillis >= removeAbandonedTimeoutMillis) {
//将连接丢弃
}
我们看看 4.2 CreateThread做的事情,它主要启动了一个线程在有连接被丢弃的时候补充线程池里的连接数,run方法如下:
public void run() {
initedLatch.countDown();
long lastDiscardCount = 0;
int errorCount = 0;
for (;;) {
// addLast
try {
lock.lockInterruptibly();
} catch (InterruptedException e2) {
break;
}
long discardCount = DruidDataSource.this.discardCount;
boolean discardChanged = discardCount - lastDiscardCount > 0;
lastDiscardCount = discardCount;
try {
boolean emptyWait = true;
// 有链接被丢弃
if (createError != null
&& poolingCount == 0
&& !discardChanged) {
emptyWait = false;
}
// 创建数量<initialSize
if (emptyWait
&& asyncInit && createCount < initialSize) {
emptyWait = false;
}
// 判断有链接被丢弃或有创建报错且poolingCount=0,且创建count<initSize.
// 如果是:则不进入下面的if分支直接创建新连接。
if (emptyWait) {
// 如果有线程等待,跳过此分支创建连接
if (poolingCount >= notEmptyWaitThreadCount //
&& (!(keepAlive && activeCount + poolingCount < minIdle))
&& !isFailContinuous()
) {
empty.await();
}
// 如果未超过最大连接数限制,则跳过await创建新连接
// 防止创建超过maxActive数量的连接
if (activeCount + poolingCount >= maxActive) {
empty.await();
continue;
}
}
} catch (InterruptedException e) {
lastCreateError = e;
lastErrorTimeMillis = System.currentTimeMillis();
if (!closing) {
LOG.error("create connection Thread Interrupted, url: " + jdbcUrl, e);
}
break;
} finally {
lock.unlock();
}
PhysicalConnectionInfo connection = null;
connection = createPhysicalConnection();
boolean result = put(connection);
if (closing || closed) {
break;
}
}
}
我删除了其中的一些代码,留下了主要的部分,可以看到,这部分代码就是一个for循环,在循环里做逻辑判断看需不需要创建新连接,需要的话就创建;不需要的话就用await方法将线程阻塞。等待下一次被调度到。
我们来详细分析一下它判断是否需要创建线程的逻辑。总结一下,它总共做了四次判断:
1、discardCount发生变化,即在上次判断之后有连接被丢弃。且createError!=null,并且poolingCount=0。若满足此三个条件,则不进行await。
2、如果进行异步init,且创建数<initialSize 即没有创建足够的连接。则不进行wait。
3、满足上述两个条件,则直接创建连接。否则,就做第三个判断:如果么有线程等待,则进行await。
4、第四个判断发生在 3步await醒来。会再做一次判断,如果还未超过最大活跃数maxActive。则创建连接
以上便主要是init方法所做的事情。我们可以看到企业应用级别的代码,有很多我们可以学到的东西。比如DCL的上锁,比如它在启动CreateConnection和DestroyConnectionThread时。用了CountdownLatch变量,来做这两个线程和init线程的同步。这些多线程同步的东西,可能对大多数人来说只是准备面试会用到的东西。但是其实在企业应用中,也是会用到的。
好了,不扯淡了。我们继续跟进去GetConnectionDirect方法,来看看线程获取连接的逻辑。下面是代码。
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++;
// 尝试次数不够,或者并且poolingCount+activeCount<maxActive就继续尝试
if (LOG.isWarnEnabled()) {
LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
}
continue;
}
throw ex;
}
// testOnBorrow逻辑
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); // 传入null,避免重复关闭
continue;
}
// testWhileIdle逻辑
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;
long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
if (timeBetweenEvictionRunsMillis <= 0) {
timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
}
if (idleMillis >= timeBetweenEvictionRunsMillis
|| idleMillis < 0 // unexcepted branch
) {
boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
}
discardConnection(poolableConnection.holder);
continue;
}
}
}
}
// 处理 removeAbandoned 逻辑
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;
}
}
这段流程图大致如下:
在 getConnectionDirect 中,druid主要做了下面的事情:
1、尝试获取连接,如果超时就判断是否达到尝试次数,如果没到且 activeCount+poolingCount < maxActive 则继续尝试,否则抛出超时异常。
2、获取到连接后判断如果开启testOnBorrow,就进行连接的校验。看连接是否正常。在mysql中是通过Connection.ping 这个方法来校验的。如果校验连接异常,则丢弃链接,重新尝试获取。
3、如果未开启 testOnBorrow,但开启testWhileIdle。则根据连接的idle时间判断,如果idle超过一定时间。则进行连接校验,判断连接是否正常。
4、连接校验完成之后,判断是否开启removeAbandoned。如果开启则将取到的连接重置ConnectedTimeNano为当前时间之后,放入activeConnections。这里我们可以回看一下DestroyConnectionThread的流程。它会判断当前时间-ConnectedTimeNono>设定值的连接为要丢弃的连接。言下之意,就是idle(空闲)超过设定值的连接将会被丢弃。
接下来,我们来跟进 getConnectionInternal 方法看看内部的流程,可以看到它其实时通过下面这一行来获取DruidConnectionHolder的。跟进takeLast,我们发现它其实就是从数组里取了一个连接出来。
if (maxWait > 0) {
holder = pollLast(nanos);
} else {
holder = takeLast();
}
总结
最后,简单的做个总结。我们主要分析了 init() 方法和 getConnectionDirect()方法:
1、druid连接池为我们提供了控制池大小的方法,即通过配置maxActive,initialSize等的大小。
2、在获取到连接的时候,druid提供连接校验连接状态的功能,包括testOnBorrow 和 testWhileIdle两个机制。不同数据库校验的方法不同。mysql是通过调用ping方法检查。
3、druid提供了连接废弃机制,通过removeAbandoned和removeAbandonedTimeoutMillis控制。被废弃的会被标记成closed。这样在下次获取连接的时候就会知道连接已关闭。
除此之外,我们看到,连接池是一个多线程使用的东西。我们可以看到源码中,DCL的加锁,ReentrantLock和CountDownLatch来做线程间的同步,Condition的wait。这些东西在平时,我们只是当作面试知识来准备。然而我们终究是要深入学习这些东西,用到这些东西的,如果不想做个简单的CRUD程序员的话。