前言
当我们使用类似于MyBatis 等ORM 框架来执行一条SQL 时,其中一步就是会从数据库连接池里获取一个连接,了解从数据库连接池获取连接的过程,对于排查SQL耗时问题具有很大的帮助。
本文将针对TomcatJdbc 数据库连接池获取连接的源码进行学习,Tomcat 版本为9.0.82。
TomcatJdbc往期文章:
接口访问超时引发的思考
搞懂TomcatJdbc之连接池初始化
正文
当初始化数据库连接池完毕后,会得到一个ConnectionPool ,后续获取连接均从得到的ConnectionPool 中获取,对应方法为ConnectionPool#getConnection,如下所示。
java
public Connection getConnection() throws SQLException {
// 先从连接池获取数据库连接
PooledConnection con = borrowConnection(-1,null,null);
// 为获取出来的连接创建代理对象
return setupConnection(con);
}
继续跟进ConnectionPool#borrowConnection方法,如下所示。
java
private PooledConnection borrowConnection(int wait, String username, String password) throws SQLException {
if (isClosed()) {
throw new SQLException("Connection pool closed.");
}
long now = System.currentTimeMillis();
// 从空闲队列获取一个连接
// 若没有空闲连接则得到null
PooledConnection con = idle.poll();
while (true) {
if (con!=null) {
// 这里主要是校验一下刚刚获取到的空闲连接
// 如果校验不通过则会重新连接一次数据库
PooledConnection result = borrowConnection(now, con, username, password);
// 借出的连接数加1
borrowedCount.incrementAndGet();
if (result!=null) {
return result;
}
}
// 执行到这里说明需要创建连接
// size表示当前连接池的总连接数
// 如果size小于配置的最大连接数则创建一个连接
if (size.get() < getPoolProperties().getMaxActive()) {
if (size.addAndGet(1) > getPoolProperties().getMaxActive()) {
size.decrementAndGet();
} else {
// 创建一个连接并校验
// 校验只有在testOnConnect配置为true时才执行
return createConnection(now, con, username, password);
}
}
// 执行到这里说明当前无空闲连接可用且无法再继续创建连接
// 那么后续就进入等待获取连接的状态
long maxWait = wait;
// wait配置为-1表示等待时间使用连接池的默认值
if (wait==-1) {
// 默认是等待30秒
maxWait = (getPoolProperties().getMaxWait()<=0)?Long.MAX_VALUE:getPoolProperties().getMaxWait();
}
// 计算还需要等待的时间
long timetowait = Math.max(0, maxWait - (System.currentTimeMillis() - now));
// 当前正在等待获取连接的线程计数加1
waitcount.incrementAndGet();
try {
// 从空闲队列中超时等待获取连接
con = idle.poll(timetowait, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
if (getPoolProperties().getPropagateInterruptState()) {
Thread.currentThread().interrupt();
}
SQLException sx = new SQLException("Pool wait interrupted.");
sx.initCause(ex);
throw sx;
} finally {
// 获取到连接或者等待超时都需要将wait减1
// 表示结束等待
waitcount.decrementAndGet();
}
if (maxWait==0 && con == null) {
if (jmxPool!=null) {
jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.POOL_EMPTY, "Pool empty - no wait.");
}
// 不等待并且没获取到连接
// 抛出异常并说明情况
throw new PoolExhaustedException("[" + Thread.currentThread().getName()+"] " +
"NoWait: Pool empty. Unable to fetch a connection, none available["+busy.size()+" in use].");
}
if (con == null) {
if ((System.currentTimeMillis() - now) >= maxWait) {
if (jmxPool!=null) {
jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.POOL_EMPTY, "Pool empty - timeout.");
}
// 等到达到了超时时间也没有获取到连接
// 则抛出异常
throw new PoolExhaustedException("[" + Thread.currentThread().getName()+"] " +
"Timeout: Pool empty. Unable to fetch a connection in " + (maxWait / 1000) +
" seconds, none available[size:"+size.get() +"; busy:"+busy.size()+"; idle:"+idle.size()+"; lastwait:"+timetowait+"].");
} else {
continue;
}
}
}
}
获取连接的流程是很清晰的,可以归纳如下。
- 首先从空闲连接队列idle 中获取一个连接。这里是通过poll() 方法获取,所以得到的连接可能为null;
- 校验获取到的连接。如果要触发校验连接的动作,需要将testOnBorrow 配置为true,如果校验成功则返回连接,如果校验失败,则会基于当前连接重连一次数据库;
- 前面如果都没有获取到连接则尝试新建一个连接。新建连接有一个前提,就是当前连接池的总连接数要小于允许的最大连接数;
- 如果无法新建连接则进入超时等待获取连接状态。如果从空闲连接队列idle 中无法获取到一个有效连接且也不允许继续创建连接,则需要在指定时间范围内从idle中等待获取连接。
总结
TomcatJdbc获取连接的整个流程图示意如下。