文章目录
- Mybatis 的连接池
- 1. 连接池介绍
- 2. Mybatis 的连接池
- 2.1 Mybatis 连接池的分类:
- 2.2 Mybatis 中使用 unpooled 配置连接池的原理分析
- 2.3 Mybatis 中使用 POOLED 配置连接池的原理分析
Mybatis 的连接池
1. 连接池介绍
我们在实际开发中都会使用连接池。
因为连接池可以减少我们获取连接所耗费的时间。
其实连接池就像一个容器,它会把连接都初始化出来放在一个容器里面,这种容器大家可以理解为一个瓶子,想要用就取一个。当一个应用或者线程取出第一个连接并且占用该连接,那么连接池中的连接的顺序就会重新排序(挨个向前进行添补,比如5号连接变为4号连接)。
当一个线程或者应用解除对一个连接的占用,那么,这个连接会重新回到连接池中。同时排在容器序列的最后一个。
容器其实就是一个集合对象,该集合必须是线程安全的,不能两个线程拿到同一个连接。该集合还必须实现队列的特性:先进先出。
2. Mybatis 的连接池
2.1 Mybatis 连接池的分类:
- Mybatis将自己的数据源分为三类:
- UNPOOLED - 不使用连接池的数据源
- POOLED - 使用连接池的数据源
- JNDI - 使用 JNDI 实现的数据源
- Mybatis 连接池提供了三种方式的配置:
- 配置的位置:
- 主配置文件中的DataSource标签,type属性就是表示采用何种连接池方法。
- type 属性的取值:
- POOLED - 采用传统的 javax.sql.DataSource 规范中的连接池,mybatis 中有针对性规范的实现
- UNPOOLED - 采用传统的获取连接的方法,虽然也实现了 javax.sql.DataSource 接口,但是并没有使用 池 的思想。
- JNDI - 采用服务器提供的 JDNI 技术实现,来获取 DataSource 对象,不同的服务器所能拿到的 DataSource 是不一样的。
注意:如果不是 web 或者 maven 的 war 工程,是不能使用的。
这里使用的 Tomcat 服务器,采用的连接池就是 dbcp 连接池。
2.2 Mybatis 中使用 unpooled 配置连接池的原理分析
- 首先打开 JDBC 连接
- 然后关闭 JDBC 中的事务的自动提交
- 准备 sql 语句
- 执行 SQL 语句然后返回结果
- 最后关闭 JDBC 的连接
上面的步骤说明,如果在主配置文件中,将配置连接池信息的 type 类型设置为 unpooled, 那么每次使用都会创建一个新的连接:
<!--配置连接池-->
<dataSource type="UNPOOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
在 Mybatis 内部分别定义实现了 javax.sql.DataSource 接口的 UnpooledDataSource, PooledDataSource 类表示 UNPOOLED、POOLED类型的数据源。
进入 UnpooledDataSource 类:
public class UnpooledDataSource implements DataSource {}
它(PooledDataSource类同样)实现了 DataSource 接口,而 DataSource 接口就是 javax.sql 的DataSource 也就是 jdbc 规范中的连接定义。其中也必定有一个方法,叫 getConnection。
然后,返回 UnpooledDataSource 类,寻找 getConnection 对象:
@Override
public Connection getConnection() throws SQLException {
return doGetConnection(username, password);
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return doGetConnection(username, password);
}
getConnection 方法在内部调用了 doGetConnection 方法,查看 doGetConnection 方法:
private Connection doGetConnection(String username, String password) throws SQLException {
Properties props = new Properties();
if (driverProperties != null) {
props.putAll(driverProperties);
}
if (username != null) {
props.setProperty("user", username);
}
if (password != null) {
props.setProperty("password", password);
}
return doGetConnection(props);
}
发现,在 doGetConnection 方法的内部,创建了一个Properties 对象,并且将用户名和密码设置进去,然后再次调用 doGetConnection 的重载方法,传递一个 props 属性过去。继续跟踪:
private Connection doGetConnection(Properties properties) throws SQLException {
initializeDriver();
Connection connection = DriverManager.getConnection(url, properties);
configureConnection(connection);
return connection;
}
在这个方法中,调用了 initializeDriver 方法,同时设置和返回了连接,而这个 initializeDriver 方法,实际上就是初始化驱动。查看 initializeDriver 方法:
private synchronized void initializeDriver() throws SQLException {
if (!registeredDrivers.containsKey(driver)) {
Class<?> driverType;
try {
if (driverClassLoader != null) {
driverType = Class.forName(driver, true, driverClassLoader);
} else {
driverType = Resources.classForName(driver);
}
// DriverManager requires the driver to be loaded via the system ClassLoader.
// http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
Driver driverInstance = (Driver)driverType.newInstance();
DriverManager.registerDriver(new DriverProxy(driverInstance));
registeredDrivers.put(driver, driverInstance);
} catch (Exception e) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
}
}
}
该方法调用了 Class.forName 方法,这个方法不就是用来注册驱动用的吗?
那么,使用 unpooled 来连接数据库就很明显了:
每次都会注册驱动,获取连接。
2.3 Mybatis 中使用 POOLED 配置连接池的原理分析
进入 PooledDataSource 类,该类在获取连接时,调用的是 popConnection 方法:
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;
while (conn == null) {
synchronized (state) {
if (!state.idleConnections.isEmpty()) {
// 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
conn = new PooledConnection(dataSource.getConnection(), this);
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()) {
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {
/*
Just log a message for debug and continue to execute the following
statement like nothing happened.
Wrap the bad connection with a new PooledConnection, this will help
to not interrupt current executing thread and give current thread a
chance to join the next competition for another valid/good database
connection. At the end of this loop, bad {@link @conn} will be set as null.
*/
log.debug("Bad connection. Could not roll back");
}
}
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
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) {
// ping to server and check the connection is valid or not
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 + poolMaximumLocalBadConnectionTolerance)) {
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;
}
这段代码描述了这么一段过程:
- 首先,如果连接为空,那么使用 synchronized 的代码块,该代码块锁定是 state (PoolState)的锁,保证了线程的安全。
- 如果池中有空闲的连接(
if (!state.idleConnections.isEmpty()){…}
),那么就拿出第一个连接,供应用使用(conn = state.idleConnections.remove(0)
)。 - 如果其中没有连接(
else{…}
),那么会进行判断:
- 如果活动连接池中活动的连接数量少于设定的最大值(
if (state.activeConnections.size() < poolMaximumActiveConnections) {…}
),在这个条件下,会创建一个连接,然后扔进活动连接池,并提供给我们使用。 - 如果活动连接池中也没有地儿了(不能再创建新的连接了),活动连接池会拿出一个 oldestActiveConnection,获得连接中最老的一个连接出来(
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
),然后进行一些相关的清理和设置操作(ooledConnection oldestActiveConnection = state.activeConnections.get(0);
后面的一大段代码,有兴趣的可以自行去分析),供我们使用。 - 通过这里的
activeConnections.get(0)
,可以发现,其实,通过 POOLED 来创建连接时,Mybatis 会创建两个池:一个空闲池,一个活动池(一样也是容器protected final List activeConnections = new ArrayList<>();
同样在 PoolState 类中定义)。