目录
- 前言
- PooledDataSource
- PoolState
- PooledConnection
- 总结
前言
前边《MyBatis原理——传统JDBC操作数据库》 提到,在MyBatis中,对于数据源DataSource有两个实现:非池化版本UnpooledDataSource和池化版本PooledDataSource。
非池化版本比较简单,和传统通过DriverManager获取数据库连接类似,只不过多了MyBatis的一些包装和校验。下边来记录下池化版本的实现。
PooledDataSource
简单看一下PooledDataSource的源码,这里只列出主要字段和构造函数:
public class PooledDataSource implements DataSource {
// 保存所有活跃连接、空闲连接以及一些相关参数
private final PoolState state = new PoolState(this);
// 持有的非池化版本对象
private final UnpooledDataSource dataSource;
public PooledDataSource() {
dataSource = new UnpooledDataSource();
}
可以看到,其实PooledDataSource内部持有一个非池化版本的DataSource,它自己本身是不干什么实事的,具体真正去获取连接等细致的活还是交给UnpooledDataSource来做。只不过我们知道,数据库连接池主要的作用就是在:
1. 在获取连接的时候,如果连接池有空闲连接,就不去获取新的物理连接;
2. 释放连接的时候,并不关闭物理连接,而是放回连接池。
下面来看看MyBatis是如何解决这两个问题的:
PoolState
前边说到,PooledDataSource持有一个PoolState字段,这个才是真正保存连接的地方,这个类的主要字段如下:
public class PoolState {
// 绑定的池化数据源
protected PooledDataSource dataSource;
// 空闲连接
protected final List<PooledConnection> idleConnections = new ArrayList<>();
// 活跃连接(正在工作的)
protected final List<PooledConnection> activeConnections = new ArrayList<>();
PoolState主要就是有这两个List,一个保存空闲连接,一个保存活跃连接,此外还有一些相关的基本统计字段,比如记录请求数量、累计请求时间等等。PooledDataSource持有它,就可以根据已有配置和相关字段的统计信息,操作这两个List,达到连接池的功能。而列表中保存的PooledConnection则是MyBatis对基本数据库连接的封装。首先简单看下PooledDataSource是如何获取连接的:
@Override
public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
private PooledConnection popConnection(String username, String password) throws SQLException {
PooledConnection conn = null;
while (conn == null) {
synchronized (state) {
// 如果存在空闲连接,则直接从空闲连接列表中获取
if (!state.idleConnections.isEmpty()) {
// Pool has available connection
conn = state.idleConnections.remove(0);
} else {
// 不存在空闲连接
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// 创建新连接
conn = new PooledConnection(dataSource.getConnection(), this);
} else {
// Cannot create new connection
// ...
}
}
if (conn != null) {
// 对连接做一些校验和配置
// ...
state.activeConnections.add(conn);
state.requestCount++;
}
}
}
if (conn == null) {
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}
这个方法本身很长,删去了一些复杂情况,仅保留最简单的内容。可以看到其实就是对PoolState中两个列表的操作:
- 如果存在空闲连接,则直接从列表上取下来一个。
- 如果不存在空闲连接,但是当前活跃连接数小于配置的最大连接数,那么新建一个连接,新建连接从本类持有的非池化DataSource获取:
dataSource.getConnection()
。并放到池中(state.activeConnections.add(conn);
)。这一点类似JDK的线程池,有常驻线程数,最大线程数等配置信息。
当然还有其他情况,比如达到最大连接数,不能再新建了,那么就需要根据PooledDataSource和PoolState中的相关配置、统计信息采取策略了,比如替换最老的连接。这里不再赘述。
可以看到,这里popConnection
返回的是PooledConnection对象,getConnection()
返回的,则是PooledConnection..getProxyConnection();
一个代理连接。所以最后的奥妙就在PooledConnection中。
PooledConnection
惯例先看下PooledConnection类的主要字段和构造方法:
class PooledConnection implements InvocationHandler {
private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };
// 绑定的数据源
private final PooledDataSource dataSource;
// 真实数据库连接
private final Connection realConnection;
// 代理数据库连接
private final Connection proxyConnection;
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);
}
public Connection getProxyConnection() {
return proxyConnection;
}
可以看到PooledConnection其实持有“两个数据库连接”。在构造方法里,一个是真实的数据库连接,由构造方法的参数传递而来,该参数由PooledDataSource持有的UnpooledDataSource中获取。另一个则是对真实连接的包装(JDK动态代理),并将自己作为代理的实现者传递进去。紧接上边得知:最终是这个代理连接被从数据库连接池中取出,它实际承担了连接对象的责任。
PooledConnection本身实现了InvocationHandler接口,所以它对连接对象搞的鬼就都在实现的invoke
方法中了:
@Override
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;
}
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);
}
}
代理方法意外的简单,大部分任务都直接委托给了真实的数据库连接(realConnection
),仅在开头对close()
方法做了拦截,如果当前调用的是close()
方法,那么仅仅将本连接放入数据库连接池:dataSource.pushConnection(this);
该push方法最终会将 连接再次放入到PoolState对象中。至此终于实现了连接池的功能。
总结
- MyBatis的池化数据源PooledDataSource持有真实数据源UnpooledDataSource和连接池容器PoolState对象,获取连接时从连接池容器中调度获取,而当需要获取真实物理连接时,仍然是委托UnpooledDataSource来做。
- PoolState持有空闲连接和活跃连接两个列表,根据配置参数和统计信息来供PooledDataSource使用。
- PooledConnection借助动态代理创建一个代理连接,并同时持有代理连接和真实连接两者,返回代理连接来执行具体数据库任务。
- 代理连接把大部分任务直接委托给了真实连接来做,而仅仅拦截
close()
方法,把自身放回到连接池容器(因为真实连接的close()
方法是断开连接)。来达到”不断开,仅放回“的功能。