Mybatis深入之数据库连接池原理
最后更新于:2022-04-01 16:30:14
# Mybatis深入之数据库连接池原理
### 简介
主要记录Mybatis数据库连接池实现原理、如何使用连接池来管理数据库连接的、连接池如何向外提供数据库连接、当外部调用使用完成之后是如何将数据库连接放回数据库连接池的。
### 准备
有前面的相关文章的铺垫、这里就不再从Mybatis数据库相关信息的初始化以及何时创建一个真正的数据库连接并且向外提供使用的。这两方面的过程可以参见[Mybatis深入之DataSource实例化过程 ](http://blog.csdn.net/crave_shy/article/details/46584803)和[Mybatis深入之获取数据库连接 ](http://blog.csdn.net/crave_shy/article/details/46597239)两篇文章。
了解Mybatis数据库连接池如何配置
~~~
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
~~~
- 标签`<dataSource type="POOLED">`指明使用Mybatis自带数据库连接池
- 标签`<transactionManager type="JDBC"/>`指明使用JDBC形式管理事务、参见[ Mybatis深入之事务管理 ](http://blog.csdn.net/crave_shy/article/details/46595391)
### 原理分析
当Mybatis初始化完成之后、根据上面的配置以及前面的文章知道、 环境中的DataSource实例是PooledDataSource、环境中的Transaction实例是JdbcTransaction。
[Mybatis深入之获取数据库连接 ](http://blog.csdn.net/crave_shy/article/details/46597239)中知道、当执行第一个sql语句时才会尝试获取数据库连接详细的可以看前面的文章、这里直接上获取数据库连接的关键代码:
~~~
public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
~~~
- 获取PooledConnection——popConnection(dataSource.getUsername(), dataSource.getPassword())
- 调用PooledConnection的getProxyConnection()方法返回真正连接的代理连接
当尝试获取数据库连接的时候会使用数据库连接池功能、详细获取数据库连接的代码[Mybatis深入之获取数据库连接 ](http://blog.csdn.net/crave_shy/article/details/46597239)有简单介绍。
在想要了解数据库连接池原理之前需要了解一个Mybatis用来存放数据库连接池状态的类:PoolState、PooledConnection以及PooledDataSource类属性以及之间的关系:
![关系图](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-08_57a858402b27d.jpg "")
- PoolState数据库连接池状态类、其内部有标识数据库连接池状态的各个属性、重点是两个属性:`List<PooledConnection> idleConnections`用于存放空闲状态的数据库连接。`List<PooledConnection> activeConnections`用于存放活动状态的连接、也就是正在使用的数据库连接。
- PoolState内部持有一个DataSource引用、在PoolState被实例化时初始化、主要用于展示当前数据库连接的一些配置信息、比如用户名、密码等。
-
PooledConnection内部持有一个PooledDataSource、同样在PooledConnection被构造时实例化PooledDataSource、其中有两个属性`private long createdTimestamp;
private long lastUsedTimestamp;`用来标识当前PooledConnection的创建时间和最后使用时间、用来判断此连接是否过期。
-
PooledDataSource 简单的线程安全的数据库连接池、对外提供数据库连接池功能。
-
### 获取PooledConnection
从上面可以知道程序中使用了数据库连接池之后、获取的数据库连接是从PooledDataSource中方法popConnection(String username, String password)获取的、获取代码:
~~~
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;
//恒成立、直到上面定义的PooledConnection被正确实例化或者程序异常中止
while (conn == null) {
//synchronized (state)是保证PooledDataSource是一个线程安全的数据库连接池的原因。
synchronized (state) {
//如果当前数据库连接池中有空闲状态的数据库连接、则直接取出一个作为当前方法执行结果返回。
if (state.idleConnections.size() > 0) {
// Pool has available connection
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
// Pool does not have available connection
//如果当前活动状态的数据库连接未达到数据库连接池容纳的最大连接数创建一个并返回
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// Can create new connection
//创建一个内部持有真正数据库连接的PooledConnection
conn = new PooledConnection(dataSource.getConnection(), this);
@SuppressWarnings("unused")
//used in logging, if enabled
//真正的数据库连接
Connection realConn = conn.getRealConnection();
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
// Cannot create new connection
//取出最先放入活动状态数据库连接集合的数据库连接
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
//如果过期、则创建一个新的、并将过期的这个从集合中移除
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// Can claim overdue connection
state.claimedOverdueConnectionCount++;
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
state.accumulatedCheckoutTime += longestCheckoutTime;
state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
oldestActiveConnection.getRealConnection().rollback();
}
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// Must wait
//线程等待
try {
if (!countedWait) {
state.hadToWaitCount++;
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
break;
}
}
}
}
//如果经过上述步骤之后数据库连接不为空、则将此连接添加到数据库连接池中并作为结果返回。
if (conn != null) {
if (conn.isValid()) {
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}
~~~
总结上述步骤
1. 先看是否有空闲(idle)状态下的PooledConnection对象,如果有,就直接返回一个可用的PooledConnection对象;否则进行第2步。
1. 查看活动状态的PooledConnection池activeConnections是否已满;如果没有满,则创建一个新的PooledConnection对象,然后放到activeConnections池中,然后返回此PooledConnection对象;否则进行第三步;
1. 看最先进入activeConnections池中的PooledConnection对象是否已经过期:如果已经过期,从activeConnections池中移除此对象,然后创建一个新的PooledConnection对象,添加到activeConnections中,然后将此对象返回;否则进行第4步。
1. 线程等待,循环2步
下面是从网络上摘抄的一个流程图:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-08_57a858404c220.jpg "")
### 获取最终数据库连接
通过PooledConnection.getProxyConnection()来获取最终数据库连接。
~~~
public Connection getProxyConnection() {
return proxyConnection;
}
~~~
这里只要弄清楚proxyConnection是什么就行,在上面一节获取PooledDataSource时候的popConnection方法中知道PooledConnection是在这个方法里面创建的、调用的都是相同的PooledConnection构造方法:
~~~
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
~~~
- 重点关注最后一行代码`this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);`
- 这里是动态代理模式来使用当前类PooledConnection来代理Connection类。
- 关于动态代理设计模式相关的这里不是重点、可以看一下[Java设计模式之代理 ](http://blog.csdn.net/crave_shy/article/details/21000887)
所以最终返回的是真正的Connection的代理类PooledConnection。至于为什么要这样做、接着看。
### 数据库连接使用完毕放回连接池
不使用数据库连接池时、正常使用数据库连接的情况下、当使用完毕之后我们就会调用其close()方法来关闭连接、避免资源浪费。但是当使用了数据库连接池之后、一个数据库连接被使用完之后就不再是调用其close方法关闭掉、而是应该将这个数据库连接放回连接池、那么我们就要拦截Connection.close()方法、将这个Connection放回连接池、而不是关闭。
使用动态代理的方式实现上述功能、PooledConnection实现了InvocationHandler接口、并提供了invoke方法的实现。当调用Connection的方法时会执行PooledConnection的invoke方法:
~~~
/*
* Required for InvocationHandler implementation.
* * @param proxy - not used
* @param method - the method to be executed
* @param args - the parameters to be passed to the method
* @see java.lang.reflect.InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
dataSource.pushConnection(this);
return null;
} else {
try {
if (!Object.class.equals(method.getDeclaringClass())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
~~~
- 拦截Connection执行方法、
- 如果是close方法、放回数据库连接池
- 如果不是、放掉、由Connection继续执行
~~~
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {
state.activeConnections.remove(conn);
if (conn.isValid()) {
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
//根据当前PooledConnection包含真正Connection重新创建一个PooledConnection并放到连接池中
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
state.idleConnections.add(newConn);
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
conn.invalidate();
if (log.isDebugEnabled()) {
log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
//立刻唤醒正在等待的线程、主要是前面从数据库连接池获取数据库连接时、如果没有现成可用数据库连接时、要等待、直到有可用的为止这个线程
state.notifyAll();
} else {
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.getRealConnection().close();
if (log.isDebugEnabled()) {
log.debug("Closed connection " + conn.getRealHashCode() + ".");
}
conn.invalidate();
}
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
}
state.badConnectionCount++;
}
}
}
~~~
1. 如果当前需要关闭的数据库连接以失效、废弃不管PoolState的badConnectionCount自增1
1. 如果当前连接有效并且当前数据库连接池中空闲连接数没有达到数据库连接池连接最大数、并且此数据库连接是所期望放回数据库连接池的。重新根据当前PooledConnection中的真正数据库连接Connection创建一个新的PooledConnection并放回数据库连接池中。
1. 如果当前空闲连接数已达到数据库连接池容量最大值、或者不是所期望的数据库连接、关闭连接
到这里、数据库连接池原理就结束了。
### 补充
Mybatis的数据库连接池简单、线程安全。当与spring结合的时候通常也可以使用第三方数据库连接池、将有关数据库连接、事务都交由spring去管理。
更多内容:[Mybatis 目录](http://blog.csdn.net/crave_shy/article/details/45825599)