MyBatis基础支持DataSource实现源码解析
作者:念念清晰 时间:2023-01-16 09:58:07
DataSource
在数据库应用中,客户端与数据库服务端建立的连接对象(Connection)是宝贵的资源,每次请求数据库都创建连接,使用完毕后会销毁连接,这是一种很浪费资源的操作。因此Java提出了DataSource接口。可以把它当作一个连接池。程序初始化时,创建一批连接放入到连接池中,如果需要请求数据库就从连接池中取出连接对象(Connection)使用完毕后把连接归还给连接池。这样就减少了每次请求都创建、销毁连接的步骤,从而提高数据库性能。
package javax.sql;
public interface DataSource extends CommonDataSource, Wrapper {
// 最重要的方法
Connection getConnection() throws SQLException;
// 其他方法不再列出
}
Java只是在JDK1.4版本发布了该接口规范。具体实现需要用户自己实现。MyBatis中提供了3种DataSource接口的实现。
UnpooledDataSource
PooledDataSource
JNDI方式的接口(不在本文讨论范围)
下面着重分析1和2这两种DataSource的实现。
UnpooledDataSource
UnpooledDataSource顾名思义,他是非池化的DataSource,说白了和普通的Connection没什么区别。通过UnpooledDataSource过去连接每次都需要重新创建一个Connection。我们来看下它的getConnection实现方法。
public Connection getConnection() throws SQLException {
return doGetConnection(username, password);
}
private Connection doGetConnection(Properties properties) throws SQLException {
initializeDriver();
Connection connection = DriverManager.getConnection(url, properties);
configureConnection(connection);
return connection;
}
在UnpooledDataSource#getConnection方法中,调用了doGetConnection方法,参数是username和password,该方法也就是通过用户名和密码获取数据库连接的意思。doGetConnection具体实现就使用了DriverManager来获取连接对象。这是JDBC原生获取连接对象的方式。
值得一说的是:UnpooledDataSource的其他方法都是基于DriverManager实现的。也就是说,使用UnpooledDataSource作为连接池的话等价于没有使用连接池。
PooledDataSource
PooledDataSource才是真正意义上的连接池,它提供了连接池的大小(默认10)、最大活跃连接数量、空闲连接数量等蚕食设置。并且对Connection对象进行了JDK * ,重写了Connection的close
方法。使得Connection对象在调用close方法是不是真正的关闭连接,而是把自定义关闭行为,MyBatis的关闭逻辑就是把Connection对象归还连接池。
我们先看下PooledDataSource的几个重要字段信息
public class PooledDataSource implements DataSource {
// PooledDataSource真正管理连接状态的是PoolState,后面会详细说明
private final PoolState state = new PoolState(this);
// UnpooledDataSource上面说过和普通的Connection无异
private final UnpooledDataSource dataSource;
//正在使用连接的数量
protected int poolMaximumActiveConnections = 10;
//空闲连接数
protected int poolMaximumIdleConnections = 5;
//在被强制返回之前,池中连接被检查的时间
protected int poolMaximumCheckoutTime = 20000;
//这是给连接池一个打印日志状态机会的低层次设置,还有重新 尝试获得连接, 这些情况下往往需要很长时间 为了避免连接池没有配置时静默失 败)。
protected int poolTimeToWait = 20000;
//发送到数据的侦测查询,用来验证连接是否正常工作,并且准备 接受请求。默认是“NO PING QUERY SET” ,这会引起许多数据库驱动连接由一 个错误信息而导致失败
protected String poolPingQuery = "NO PING QUERY SET";
//开启或禁用侦测查询
protected boolean poolPingEnabled = false;
//用来配置 poolPingQuery 多次时间被用一次
protected int poolPingConnectionsNotUsedFor = 0;
private int expectedConnectionTypeCode;
}
这些字段主要记录了连接池的重要信息:连接池大小、空闲时最大连接数、最大活跃连接数、超时时间等。而整整揭开PooledDataSource获取连接对象的神秘面纱还需要介绍两个类。PooledConnection和PoolState
PooledConnection
PooledConnection实现了InvocationHandler接口,他是用来做JDK * 的。前文提到过,mybatis使用JDK * 重写了Connection对象的close方法,就是在该类中实现的逻辑。该类有几个重要属性。
private PooledDataSource dataSource; // dataSource的副本
private Connection realConnection; // 真实连接对象
private Connection proxyConnection; // 实际返回的代理对象
接下来来看下代理对象的invoke方法是如何重写close方法的。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//如果调用close的话,忽略它,反而将这个connection加入到池中
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
dataSource.pushConnection(this);
return null;
}
return method.invoke(realConnection, args);
// 其他逻辑省略....
}
在invoke方法中判断下执行的方法名称是否是Close,如果是,就不再执行原来的close方法了,而是执行PooledDataSource 的pushConnection方法!从方法名可以看出方法的作用是:把连接push到连接池PooledDataSource 中。pushConnection的逻辑后文详细说明
PoolState
上文提到PooledDataSource并不管理连接对象。那么程序初始化的时候创建的一批连接存放到哪里了呢?答案是存在PoolState对象中,而PooledDataSource有一个属性就是PoolState。也就是说PooledDataSource是通过PoolState来管理连接池的。
一批连接在Java中就是一个List集合嘛。那么我们想一下PoolState都需要怎么管理连接呢?首先根据连接的状态,可以把连接分为2种
空闲连接
protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
活跃连接
protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
PoolState中两个List属性分别存储空闲连接和活跃连接。需要连接的时候就从idleConnections
列表中取,关联连接时就把连接从activeConnections
中移到idleConnections
中。
PoolState中还有一些其他的统计信息字段,比如 请求次数、请求的总时间、总连接数等这些属性比较简单就不再列出了
获取连接
介绍完PooledConnection和PoolState这两个类后,我们来看下PooledDataSource是怎么获取连接的。获取连接的逻辑在PooledDataSource#getConnection方法中,getConnection方法只是一个壳子,具体调用逻辑在popConnection方法。我们来看一下(我只列出了重要逻辑)
public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
private PooledConnection popConnection(String username, String password) throws SQLException {
//最外面是while死循环,如果一直拿不到connection,则不断尝试
while (conn == null) {
synchronized (state) {
if (!state.idleConnections.isEmpty()) {
//如果有空闲的连接的话,返回第一个空闲连接
conn = state.idleConnections.remove(0);
} else {
//如果没有空闲的连接
if (state.activeConnections.size() < poolMaximumActiveConnections) {
//如果activeConnections太少,那就new一个PooledConnection
conn = new PooledConnection(dataSource.getConnection(), this);
} else {
//如果activeConnections已经很多了,那不能再new了
//取得activeConnections列表的第一个(最老的)
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
//如果checkout时间过长,则这个connection标记为overdue(过期)
//删掉最老的连接,然后再new一个新连接
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
oldestActiveConnection.invalidate();
} else {
//如果checkout时间不够长,没办法,只能等待,在此分支会记录一些统计信息
}
}
}
if (conn != null) {
if (conn.isValid()) {
//如果已经拿到connection,则记录一些统计信息
} else {
//如果没拿到,统计信息:坏连接+1
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
//如果好几次都拿不到,就放弃了,抛出异常
}
}
}
}
return conn;
}
在popConnection中
从PoolState对象的空闲连接列表中获取连接,如果有空闲连接就返回。
从PoolState对象的活跃连接列表中获取连接,如果连接数小于最大活跃数,则new一个连接返回。如果没有只能等待其他线程释放连接再进行获取
无论是否获取到连接,对连接进行一些信息统计并记录到PoolState对象中。一旦尝试获取连接的时间超过了阈值,就会放弃获取连接抛出异常
关闭连接
在PooledConnection小节中见到,PooledConnection重写了Connection的close方法。当调用Connection的close方法时真正执行的逻辑是PooledDataSource的pushConnection方法。该代码逻辑很简单,大体上说,就是把连接从活跃列表中删除,加入到空闲列表中。具体实现如下
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {
//先从activeConnections中删除此connection
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();
}
//new一个新的Connection,加入到idle列表
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
state.idleConnections.add(newConn);
//通知其他线程可以来抢connection了
state.notifyAll();
} else {
//否则,即空闲的连接已经足够了
state.accumulatedCheckoutTime += conn.getCheckoutTime();
//那就将connection关闭就可以了,获取真正的connection对象并且关闭
conn.getRealConnection().close();
conn.invalidate();
}
}
}
}
关闭过程:
空闲连接数<最大空闲连接数 则新建一个连接存放到PoolState的空闲列表中并通知其他线程可以来抢Connection对象
如果PoolState的空闲列表是满的,那只能获取真正的connection对象并将其关闭了。
小结
PooledDataSource真正意义上实现了DataSource接口。具有连接池的意义
PooledDataSource通过PooledConnection和PoolState来管理连接池中的连接
PooledConnection重写了Connection对象的close方法。调用Connection的close方法时并不会真正的关闭连接,而是先要进行归还连接的操作。
PoolState是对连接列表状态的管理。它有两个List属性,分别存储了活跃连接列表和空闲连接列表
DataSourceFactory
获取MyBatis提供的DataSource实现,需要通过工厂DataSourceFactory
接口来获取。在这里MyBatis使用了工厂方法模式。DataSourceFactory有两个实现类。分别是
UnpooledDataSourceFactory
PooledDataSourceFactory
我们首先来看下工厂接口定义
public interface DataSourceFactory {
//设置属性,被XMLConfigBuilder所调用
void setProperties(Properties props);
//生产数据源,直接得到javax.sql.DataSource
DataSource getDataSource();
}
其中最重要的方法就是getDataSource,它很直观,通过工厂对象的该方法可以获取DataSource实现。
UnpooledDataSourceFactory
UnpooledDataSourceFactory获取dataSource的方法非常简单直观。
首先,构造方法里里new了一个UnpooledDataSource对象存放到工厂的属性中
然后,getDataSource直接返回该对象即可。具体实现如下
public class UnpooledDataSourceFactory implements DataSourceFactory {
protected DataSource dataSource;
public UnpooledDataSourceFactory() {
this.dataSource = new UnpooledDataSource();
}
public DataSource getDataSource() {
return dataSource;
}
}
PooledDataSourceFactory
PooledDataSourceFactory就有意思了,想偷懒,直接继承自UnpooledDataSourceFactory。只需要在构造方法中new一个PooledDataSource对象,再通过getDataSource方法获取即可。
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
//数据源换成了PooledDataSource
public PooledDataSourceFactory() {
this.dataSource = new PooledDataSource();
}
}
来源:https://juejin.cn/post/7195883375995289657