MyBatis 是一个流行的持久层框架,提供了一个简单且灵活的方式来访问数据库,它内置了一个连接池来管理数据库连接。这篇文章,我们将深入分析 MyBatis 内置的连接池源码,包括设计原理、类结构,以及核心方法的实现等。
一、连接池原理
MyBatis 内置的连接池采用了传统的 Java JDBC 连接方式,它负责管理数据库连接的创建、维护和销毁。连接池的设计可以避免每次请求数据库时都重新创建连接,从而提高性能。
- 连接的创建与管理:MyBatis 使用PooledDataSource类来创建和管理数据库连接。该类实现了DataSource接口,并使用标准的 JDBC API 来获取连接。它会维护一个连接的池,每当请求新的连接时,首先会检查连接池中是否有可用的连接,如果有,则直接返回;如果没有,则创建新的连接。
- 连接的复用:通过连接池,MyBatis可以重用已经创建的连接。当请求完成后,该连接不会被关闭,而是返回到连接池中,以便后续请求再次使用。这种方式显著减少了创建连接的开销。
- 连接的关闭与回收:在应用程序的生命周期中,连接池还需要定期检查并关闭超时未使用的连接,以维护资源的有效性。在 MyBatis中,可通过设置最大连接数、最大空闲时间等参数来控制。
二、核心源码分析
Mybatis的源码类整体结构如下图(本文基于 MyBatis 3.5.7):
下面,我们具体分析几个核心的类:
1. PooledDataSource
在 MyBatis 中,PooledDataSource 是其内置连接池的实现,负责管理可重用数据库连接。其核心概念是通过维护一组连接的池,减少频繁创建和销毁连接所带来的性能开销。
PooledDataSource 类在 MyBatis 的 org.apache.ibatis.datasource 包中实现,与多个其他类一起协同工作来管理连接。其结构如下:
- PooledDataSource:主要的连接池类,管理连接的创建、分配和回收。
- PooledConnection:对每个 JDBC 连接的包装类,封装了 JDBC Connection 对象,管理连接的状态。
2.初始化连接池
在构造函数中,PooledDataSource 提供了多种方式来初始化连接池,包括一些基本参数,如数据库 URL、用户名和密码,连接池的大小(默认为10)以及最大闲置时间(默认为30秒),这些参数可以通过 MyBatis 配置文件设置。
3.获取连接
在 PooledDataSource 类中,定义了两个关键的方法来获取数据库连接:
@Override
public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return popConnection(username, password).getProxyConnection();
}
这两个方法内部都会调用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) {
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;
}
popConnection方法会检查当前池中是否有可用连接:
- 如果连接池中有空闲连接,就从 idleConnections 列表中移除并返回该连接。这里使用 remove(0) 方法获取并移除列表的第一个元素,确保获取的是最旧的连接。
- 如果没有空闲连接,且当前活跃连接数小于最大活跃连接数,可以创建新的连接。
- 如果已有的活跃连接已达到最大数量,且没有空闲连接,方法会检查最旧的活跃连接的检出时间。
- 如果超时,则将其声明为过期连接,并尝试获取其持有的资源;否则,当前线程必须等待可用连接。
- 如果必须等待连接,方法会调用 wait(),使当前线程挂起,同时记录等待时间和次数。
- 在成功获得连接后,还需检查其有效性。如果有效,则进行一些准备工作,如回滚事务、设置连接属性等。
- 若连接无效,则增加坏连接计数,并尝试重新获取连接。
- 如果无法得到有效连接,抛出 SQLException 表示连接池发生了严重错误。
- 如果一切正常则返回连接
总结:
- popConnection方法负责从 MyBatis 的连接池中获取连接,详细设计考虑了多个方面,包括:
- 连接的复用与生命周期管理:通过维护空闲连接和活跃连接,有效控制连接的生命周期,减少开销。
- 并发控制:使用 synchronized 确保在多线程环境下的安全性,避免连接状态混乱。
- 连接的有效性检查:在获取连接后,确保连接仍是有效的,避免因无效连接导致的错误。
- 超时管理与连接等待:通过定义一定的等待策略,避免因连接争用引发的资源竞争。
4.关闭连接
关闭连接池的核心源码如下:
当需要关闭连接池时,forceCloseAll 方法会被调用,它会遍历连接池中的所有PooledConnection实例并调用它们的 forceClose 方法,确保所有连接被关闭并释放资源。
三、PooledConnection
PooledConnection 类是包装 JDBC Connection 对象的类,提供了额外的功能和方法。其核心代码如下:
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);
}
PooledConnection 维护着一个真实的 JDBC 连接实例,以及一个可用状态标志。可以通过 isAvailable 来判断连接是否可以使用。
四、连接池的管理逻辑
在 PooledDataSource 的实现中,连接的获取和归还之间包含了若干管理逻辑,以确保连接的有效性和可用性:
- 检查连接可用性:在连接使用之前,会检查连接状态,确保其未被占用。
- 连接超时管理:将连接的最大闲置时间作为管理参数,如果连接在一定时间内未使用,则通过定时任务回收这些连接。
- 连接数限制:池中最大连接数的限制可以防止过多的连接被创建,避免因资源浪费而降低性能。
五、优缺点
优点:
- 简单易用:MyBatis 自带连接池实现简单,适合快速开发和少量数据库操作的场景。
- 无外部依赖:作为 MyBatis 的一部分,使用内置连接池不需要额外添加依赖。
缺点:
- 功能单一:与其他成熟的连接池相比,MyBatis 自带连接池的功能较为简单,缺乏一些高级特性,如连接健康检查、连接存活时间管理等。
- 性能限制:在高并发环境下,MyBatis 内置连接池可能遭遇性能瓶颈,易出现连接争用或等待。
六、总结
这篇文章,我们详细地分析了MyBatis内置线程池的原理以及核心源码分析,内置的连接池适合于简单的应用场景,随着项目复杂度的增加,特别是在高并发的情况下,使用如 HikariCP、C3P0 等更成熟的连接池实现。