在 MyBatis 中,使用 PooledDataSource 数据源作为连接池对象,在连接池中存储的是 PooledConnection 对象。通过动态代理,实现对原始连接对象的复用,以及多线程下数据库连接之间的隔离。
1. 数据源配置
在 mybatis-config.xml 配置文件中,可以通过设置 dataSource 标签来配置数据源。
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
以下配置说明,截取自官方文档-XML配置-environments
dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。
有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"):
UNPOOLED
这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。
性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。UNPOOLED 类型的数据源仅仅需要配置以下 5 种属性:
- driver – 这是 JDBC 驱动的 Java 类全限定名(并不是 JDBC 驱动中可能包含的数据源类)。
- url – 这是数据库的 JDBC URL 地址。
- username – 登录数据库的用户名。
- password – 登录数据库的密码。
- defaultTransactionIsolationLevel – 默认的连接事务隔离级别。
- defaultNetworkTimeout – 等待数据库操作完成的默认网络超时时间(单位:毫秒)。查看 java.sql.Connection#setNetworkTimeout() 的 API 文档以获取更多信息。
作为可选项,你也可以传递属性给数据库驱动。只需在属性名加上“driver.”前缀即可,例如:
- driver.encoding=UTF8
这将通过 DriverManager.getConnection(url, driverProperties) 方法传递值为 UTF8 的 encoding 属性给数据库驱动。
POOLED
这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。
这种处理方式很流行,能使并发 Web 应用快速响应请求。除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源:
- poolMaximumActiveConnections – 在任意时间可存在的活动(正在使用)连接数量,默认值:10
- poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
- poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)
- poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。
- poolMaximumLocalBadConnectionTolerance – 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和。 默认值:3(新增于 3.4.5)
- poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息。
- poolPingEnabled – 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
- poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。
JNDI
这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。这种数据源配置只需要两个属性:
- initial_context – 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
- data_source – 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
和其他数据源配置类似,可以通过添加前缀“env.”直接把属性传递给 InitialContext。比如:
- env.encoding=UTF8
这就会在 InitialContext 实例化时往它的构造方法传递值为 UTF8 的 encoding 属性。
第三方数据源
你可以通过实现接口 org.apache.ibatis.datasource.DataSourceFactory 来使用第三方数据源实现:
public interface DataSourceFactory { void setProperties(Properties props); DataSource getDataSource(); }
org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory 可被用作父类来构建新的数据源适配器,比如下面这段插入 C3P0 数据源所必需的代码:
import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory; import com.mchange.v2.c3p0.ComboPooledDataSource; public class C3P0DataSourceFactory extends UnpooledDataSourceFactory { public C3P0DataSourceFactory() { this.dataSource = new ComboPooledDataSource(); } }
为了令其工作,记得在配置文件中为每个希望 MyBatis 调用的 setter 方法增加对应的属性。 下面是一个可以连接至 PostgreSQL 数据库的例子:
<dataSource type="org.myproject.C3P0DataSourceFactory"> <property name="driver" value="org.postgresql.Driver"/> <property name="url" value="jdbc:postgresql:mydb"/> <property name="username" value="postgres"/> <property name="password" value="root"/> </dataSource>
2. 源码分析
本节以 <dataSource type="POOLED">
配置为例,探究 PooledDataSource 的实现原理。
2.1 PooledDataSource 的实现原理
构造函数
PooledDataSource 的构造函数如下,可以看到在创建 PooledDataSource 对象的时候,会创建 UnpooledDataSource 对象。
同时,在实例化 PooledDataSource 对象的时候,会创建 PoolState 实例。
private final PoolState state = new PoolState(this); // 用于存储数据库连接对象
private final UnpooledDataSource dataSource; // 用于创建数据库连接对象
private int expectedConnectionTypeCode; // 数据库连接标识,url+username+password 字符串的哈希值
public PooledDataSource() {
dataSource = new UnpooledDataSource();
}
public PooledDataSource(UnpooledDataSource dataSource) {
this.dataSource = dataSource;
}
public PooledDataSource(String driver, String url, String username, String password) {
dataSource = new UnpooledDataSource(driver, url, username, password);
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
}
public PooledDataSource(String driver, String url, Properties driverProperties) {
dataSource = new UnpooledDataSource(driver, url, driverProperties);
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
}
public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, String username, String password) {
dataSource = new UnpooledDataSource(driverClassLoader, driver, url, username, password);
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
}
public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, Properties driverProperties) {
dataSource = new UnpooledDataSource(driverClassLoader, driver, url, driverProperties);
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
}
数据库连接-原始对象
数据库的连接地址、用户名密码信息,保存在 UnpooledDataSource 对象之中。
在 PooledDataSource 对象中,为什么要保存一个 UnpooledDataSource 对象呢?
这是为了利用 UnpooledDataSource 来向数据库建立连接。
比如在使用 MySQL 驱动的情况下,会向 MySQL 服务器建立 Socket 连接,并返回一个 com.mysql.cj.jdbc.ConnectionImpl 连接对象。
org.apache.ibatis.datasource.unpooled.UnpooledDataSource#getConnection()
org.apache.ibatis.datasource.unpooled.UnpooledDataSource#doGetConnection(java.lang.String, java.lang.String)
org.apache.ibatis.datasource.unpooled.UnpooledDataSource#doGetConnection(java.util.Properties)
private Connection doGetConnection(Properties properties) throws SQLException {
initializeDriver();
Connection connection = DriverManager.getConnection(url, properties); // 利用数据库驱动包,创建连接对象
configureConnection(connection);
return connection;
}
数据库连接-代理对象
PooledDataSource 中的 PoolState,是一个内部类,用于存储数据库连接对象,以及记录统计信息。
数据库连接池的大小,由 PoolState 中两个集合的容量决定:
- 空闲连接集合中,存储的是没有被使用的、可以直接拿去使用的连接。
- 活动连接集合中,存储的是正在使用中的连接。
public class PoolState {
protected PooledDataSource dataSource;
protected final List<PooledConnection> idleConnections = new ArrayList<>(); // 空闲的连接
protected final List<PooledConnection> activeConnections = new ArrayList<>(); // 活动的连接
protected long requestCount = 0; // 请求次数
protected long accumulatedRequestTime = 0; // 总请求时间
protected long accumulatedCheckoutTime = 0; // 总的检出时间(从池中取出连接,称为检出)
protected long claimedOverdueConnectionCount = 0; // 声明为已过期的连接数
protected long accumulatedCheckoutTimeOfOverdueConnections = 0; // 总的已过期的连接数
protected long accumulatedWaitTime = 0; // 总等待时间
protected long hadToWaitCount = 0; // 要等待的次数
protected long badConnectionCount = 0; // 坏的连接次数
public PoolState(PooledDataSource dataSource) {
this.dataSource = dataSource;
}
}
PoolState 中并不是存储原始的连接对象,如 com.mysql.cj.jdbc.ConnectionImpl,而是存储 PooledConnection 对象。
这里采用了 JDK 的动态代理,每次创建 PooledConnection 的时候,都会为原始的连接对象,创建一个动态代理。
使用代理的目的是,改变 Connection 的行为:
- 把连接关闭行为 Connection#close,改为将连接归还连接池。
- 每次使用连接之前,检查 PooledConnection#valid 属性是否有效(只是检查代理对象是否有效,并没有检查原始连接)。
org.apache.ibatis.datasource.pooled.PooledConnection
class PooledConnection implements InvocationHandler { // 相当于一个工具类
private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };
private final int hashCode;
private final PooledDataSource dataSource;
private final Connection realConnection; // 原始类-数据库连接
private final Connection proxyConnection; // 代理类-数据库连接
private long checkoutTimestamp; // 从连接池中检出的时间戳
private long createdTimestamp; // 创建的时间戳
private long lastUsedTimestamp; // 上一次使用的时间戳
private int connectionTypeCode;
private boolean valid; // 连接是否有效
/**
* Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in.
*
* @param connection
* - the connection that is to be presented as a pooled connection
* @param dataSource
* - the dataSource that the connection is from
*/
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); // JDK 动态代理
}
/**
* 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[])
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 代理方法
String methodName = method.getName();
if (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);
}
}
private void checkConnection() throws SQLException {
if (!valid) {
throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
}
public boolean isValid() { // 校验连接是否有效
return valid && realConnection != null && dataSource.pingConnection(this);
}
PooledDataSource#pingConnection
PooledDataSource 的使用过程中,会调用 PooledConnection#isValid 方法来检查连接是否有效。
PooledDataSource 类中与连接检查相关的属性:
// 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。
protected String poolPingQuery = "NO PING QUERY SET";
// 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
protected boolean poolPingEnabled;
// 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。
protected int poolPingConnectionsNotUsedFor;
当满足以下条件时,会向数据库发送 poolPingQuery 所配置的 SQL 语句。
- 数据库连接未关闭。
- 在 MyBatis XML 中配置 poolPingEnabled 为 true。
- 距离上一次使用连接的时间,大于连接检查频率。
org.apache.ibatis.datasource.pooled.PooledDataSource#pingConnection
/**
* Method to check to see if a connection is still usable
*
* @param conn
* - the connection to check
* @return True if the connection is still usable
*/
protected boolean pingConnection(PooledConnection conn) { // 校验连接是否有效
boolean result = true;
try {
result = !conn.getRealConnection().isClosed(); // 校验数据库连接会话是否已关闭 eg. com.mysql.cj.jdbc.ConnectionImpl.isClosed
} catch (SQLException e) {
if (log.isDebugEnabled()) {
log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
}
result = false;
}
if (result && poolPingEnabled && poolPingConnectionsNotUsedFor >= 0 // 配置了需要检查连接
&& conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) { // 距离上一次使用连接的时间,大于连接检查频率
try {
if (log.isDebugEnabled()) {
log.debug("Testing connection " + conn.getRealHashCode() + " ...");
}
Connection realConn = conn.getRealConnection();
try (Statement statement = realConn.createStatement()) {
statement.executeQuery(poolPingQuery).close(); // 发送简单语句,检查连接是否有效
}
if (!realConn.getAutoCommit()) {
realConn.rollback();
}
result = true;
if (log.isDebugEnabled()) {
log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
}
} catch (Exception e) {
log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage()); // 连接检查失败
try {
conn.getRealConnection().close(); // 尝试关闭连接
} catch (Exception e2) {
// ignore
}
result = false;
if (log.isDebugEnabled()) {
log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
}
}
}
return result;
}
PooledConnection 对象中会记录上一次使用连接的时间戳(毫秒级)。
org.apache.ibatis.datasource.pooled.PooledConnection#getTimeElapsedSinceLastUse
/**
* Getter for the time since this connection was last used.
*
* @return - the time since the last use
*/
public long getTimeElapsedSinceLastUse() {
return System.currentTimeMillis() - lastUsedTimestamp;
}
PooledDataSource#popConnection
从池中取出数据库连接,代码流程:
- 采用 while 循环从数据库连接池中取出连接(该操作称为检出 checkout),每次循环开始都需要获取
PoolState state
对象锁。 检测 PoolState 中的空闲连接集合和活动连接集合,并从中获取连接对象,分为几种情况:
2.1 空闲连接集合非空,则从中取出一个连接。
2.2 空闲连接集合为空,活动连接集合未满,则利用数据库驱动包建立新连接,并包装为 PooledConnection 对象(生成动态代理)。
2.3 空闲连接集合为空,活动连接集合已满,则需要对最早的连接进行检查:2.3.1 如果该连接已超时(代理对象的检出时间大于 poolMaximumCheckoutTime,但是原始连接可能还存活),此时将代理对象 PooledConnection 标记为失效,将原始连接封装为新的 PooledConnection 对象。 2.3.2 如果该连接未超时,则当前的检出线程进入等待。
- 来到这一步,说明从 PoolState 检出 PooledConnection 对象成功,需要检查该连接是否有效:
3.1 如果连接有效,则设置相关时间戳,并存入活动连接集合,结束 while 循环。
3.2 如果连接无效,重新进入 while 循环。
3.3 重新进入 while 循环的次数是有限的,不可超过(空闲连接数 + 坏连接忍受阈值),否则抛出异常。
完整代码如下:
org.apache.ibatis.datasource.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 // 将坏连接包装为一个新的 PooledConnection 对象
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); // 后续需要识别为坏连接!怎么识别?通过 PooledConnection#isValid
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); // 等待直到超时,或者被其他线程唤醒(见 PooledDataSource#pushConnection)。接着进入下一次 while 循环
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)); // 设置连接标识:url+username+password 字符串的哈希值
conn.setCheckoutTimestamp(System.currentTimeMillis()); // 设置检出时间,注意,这里是从数据库连接池中取出的时间戳!而不是与数据库建立连接的时间!
conn.setLastUsedTimestamp(System.currentTimeMillis()); // 设置最后一次使用时间
state.activeConnections.add(conn); // 加入活跃集合(1. 把原连接对象从空闲集合移动到活跃集合;2. 从活跃集合中取出超时连接,又放回活跃集合)
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;
}
在检出连接的过程中,会利用 PoolState 来记录一些总的耗时。
org.apache.ibatis.datasource.pooled.PoolState
protected long requestCount = 0; // 请求次数
protected long accumulatedRequestTime = 0; // 总请求时间
protected long accumulatedCheckoutTime = 0; // 总的检出时间(从池中取出连接,称为检出)
protected long claimedOverdueConnectionCount = 0; // 声明为已过期的连接数
protected long accumulatedCheckoutTimeOfOverdueConnections = 0; // 总的已过期的连接数
protected long accumulatedWaitTime = 0; // 总等待时间
protected long hadToWaitCount = 0; // 要等待的次数
protected long badConnectionCount = 0; // 坏的连接次数
而在 PooledDataSource 对象中,会设置空闲连接集合、活动连接集合的容量,以及一些最大时间限制。
org.apache.ibatis.datasource.pooled.PooledDataSource
protected int poolMaximumActiveConnections = 10; // 在任意时间可存在的活动(正在使用)连接数量
protected int poolMaximumIdleConnections = 5; // 任意时间可能存在的空闲连接数
protected int poolMaximumCheckoutTime = 20000; // 在被强制返回之前,池中连接被检出的时间。默认值:20000 毫秒(即 20 秒)
protected int poolTimeToWait = 20000; // 这是一个底层设置,如果获取连接花费的相当长的时间,它会给连接池打印状态日志,并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志)
protected int poolMaximumLocalBadConnectionTolerance = 3; // 这是一个关于坏连接容忍度的底层设置,作用于每一个尝试从缓存池获取连接的线程。如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和
PooledDataSource#pushConnection
将连接归还数据库连接池。
代码流程:
- 获取 PoolState 对象锁。
- 从活跃连接集合中移除 PooledConnection,并检查连接是否有效。
- 若连接有效,则判断空闲集合是否已满:
3.1 空闲集合未满,将原始连接封装为新的 PooledConnection 对象,加入空闲集合。
3.2 空闲集合已满,则关闭连接,不再复用。
可以看到,每次归还数据库连接,实际上是归还原始的 com.mysql.cj.jdbc.ConnectionImpl 连接对象,而 PooledConnection 生成的代理对象则是用完就丢,并且设置为失效状态,避免在复用时影响其他线程。
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {
state.activeConnections.remove(conn); // 从活跃连接集合中移除
if (conn.isValid()) { // 校验连接是否有效,若有效则进入下一步
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { // 空闲连接集合未满,并且连接标识一致(url+username+password),则需要加入空闲连接集合
state.accumulatedCheckoutTime += conn.getCheckoutTime(); // 累加总的检出时间(记录连接从出池到入池的总时间)(有效的连接从池中取出时,会记录检出时间戳)
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback(); // 把之前的事务回滚,避免对下次使用造成影响
}
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); // 为原始连接生成新的 PooledConnection 对象
state.idleConnections.add(newConn); // 加入空闲连接集合(注意这里不是把旧的 PooledConnection 从活跃集合移动到空闲集合)
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
conn.invalidate(); // 将旧的 PooledConnection 对象设为失效,因为用户可以直接拿到这个实例,避免后续仍使用这个实例操作数据库
if (log.isDebugEnabled()) {
log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
state.notifyAll(); // 唤醒等待获取数据库连接的线程,见 PooledDataSource#popConnection。被唤醒后只有一个线程会获得对象锁。
} 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++;
}
}
}
2.2 PooledDataSource 的使用流程
PooledDataSource 的使用流程如下:
- 解析 mybatis-config.xml 配置文件时,创建 PooledDataSource 连接池对象。
- 开启 SqlSession 数据库会话时,创建 JdbcTransaction 事务对象,利用 JdbcTransaction 来维护对数据库连接池的存取操作。
- 在一次会话中,JdbcTransaction 只会向连接池获取一个连接。在该会话范围之内,读写数据库的操作都通过该连接来完成。
- 关闭 SqlSession 数据库会话时,向数据库连接池归还连接。
数据源配置解析
使用 SqlSessionFactoryBuilder 解析 mybatis-config.xml 配置文件的时候,会解析其中的 environments 标签。
调用链如下:
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(Reader, String, java.util.Properties)
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse()
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration(org.apache.ibatis.parsing.XNode)
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); // 实例化事务工厂
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); // 实例化数据库连接池工厂
DataSource dataSource = dsFactory.getDataSource(); // 从数据库连接池工厂中,获取数据源对象。一个 environment 标签只有一个数据源!
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build()); // 将事务工厂、数据源对象注册到 Configuration 对象中
break;
}
}
}
}
数据源初始化
由于配置的是 <dataSource type="POOLED">
,XML 解析得到的是 PooledDataSourceFactory 对象。
org.apache.ibatis.builder.xml.XMLConfigBuilder#dataSourceElement
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type"); // eg. "POOLED"
Properties props = context.getChildrenAsProperties();
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
factory.setProperties(props); // 将配置文件中的数据库连接信息,写入 DataSourceFactory 中的 DataSource 属性
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
PooledDataSourceFactory 类内容如下,在构造函数中会创建 PooledDataSource 对象:
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
public PooledDataSourceFactory() {
this.dataSource = new PooledDataSource();
}
}
PooledDataSourceFactory 继承体系:
开启会话
开启 SqlSession 会话时,会从事务工厂中创建事务对象 Transaction,并将 DataSource 对象传递给它。
SqlSession sqlSession = sqlSessionFactory.openSession();
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 通过事务工厂,实例化 Transaction 事务对象
final Executor executor = configuration.newExecutor(tx, execType); // 实例化 Executor 执行器对象,通过它来执行 SQL,支持插件扩展
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
由于配置的是 <transactionManager type="JDBC">
,这里得到的是 JdbcTransactionFactory,因此创建 JdbcTransaction 事务对象。
org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory#newTransaction
@Override
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
return new JdbcTransaction(ds, level, autoCommit);
}
关于事务管理器(transactionManager)的官方说明:
在 MyBatis 中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]"):
- JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
- MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。
如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。
连接获取
执行 SQL 查询时,会从数据库连接池 PooledDataSource 中,获取数据库连接对象 Connection。
Student student01 = sqlSession.selectOne("selectByPrimaryKey", 1);
调用链如下:
org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList
org.apache.ibatis.executor.SimpleExecutor#doQuery
org.apache.ibatis.executor.SimpleExecutor#prepareStatement
org.apache.ibatis.executor.BaseExecutor#getConnection
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection(); // 从事务对象中,获取连接对象
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
JdbcTransaction 对象在一次会话中是单例的!因此在同一次会话使用同一个数据库连接。
org.apache.ibatis.transaction.jdbc.JdbcTransaction#getConnection
protected Connection connection;
@Override
public Connection getConnection() throws SQLException {
if (connection == null) { // 为空的时候,才从连接池中获取连接
openConnection();
}
return connection;
}
实际上是从数据源对象 PooledDataSource 中获取连接,这里得到的是一个代理对象。
org.apache.ibatis.transaction.jdbc.JdbcTransaction#openConnection
org.apache.ibatis.datasource.pooled.PooledDataSource#getConnection
@Override
public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
连接归还
关闭数据库会话时,将数据库连接归还给连接池。
sqlSession.close();
调用链如下:
org.apache.ibatis.session.defaults.DefaultSqlSession#close
org.apache.ibatis.executor.BaseExecutor#close
org.apache.ibatis.transaction.jdbc.JdbcTransaction#close
org.apache.ibatis.datasource.pooled.PooledConnection#invoke
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 代理方法
String methodName = method.getName();
if (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);
}
}
3. 测试用例
3.1 关闭会话后,连接的有效性验证
关闭 SQLSession 之后,数据库连接对象表现为“已失效”。
实际上,此时只是数据库连接的代理对象是失效的,数据库连接的原始对象依旧有效,并已归还到数据库连接池中。
@Test
public void valid() throws SQLException {
// 建立会话
SqlSession sqlSession = sqlSessionFactory.openSession();
Connection connection = sqlSession.getConnection();
boolean valid = connection.isValid(1000);
System.out.println("valid = " + valid);
// 关闭会话
sqlSession.close();
// assert 无效的连接
connection.isValid(1000);
}
执行结果如下:
2021-08-24 22:45:52,618 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
2021-08-24 22:45:53,291 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 1847637306.
2021-08-24 22:45:53,292 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6e20b53a]
valid = true
2021-08-24 22:45:53,293 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6e20b53a]
2021-08-24 22:45:53,294 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6e20b53a]
2021-08-24 22:45:53,294 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Returned connection 1847637306 to pool.
java.sql.SQLException: Error accessing PooledConnection. Connection is invalid.
3.2 使用之前,连接有效性的检查
从源码阅读可知,在使用数据库连接之前,有两个位置会检查连接的有效性:
- 从连接池中检出数据库连接,会调用 PooledConnection#isValid 方法检查连接是否有效,此时会向数据库发送 ping 语句检查原始连接是否有效。
org.apache.ibatis.datasource.pooled.PooledConnection#isValid
public boolean isValid() {
return valid && realConnection != null && dataSource.pingConnection(this);
}
- 对于已经检出的连接,每次使用前会检查 PooledConnection#valid 属性是否有效(并没有检查原始连接),防止当前 PooledConnection 被其他线程置为无效。
org.apache.ibatis.datasource.pooled.PooledConnection#invoke
org.apache.ibatis.datasource.pooled.PooledConnection#checkConnection
private void checkConnection() throws SQLException {
if (!valid) {
throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
}
测试用例如下:
修改 mybatis-config.xml 配置,将 poolPingQuery
修改为错误的语句,模拟向数据库检查原始连接 ping 失败的场景。
<environment id="development">
<transactionManager type="JDBC"/>
<!-- POOLED 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="poolPingEnabled" value="true"/>
<property name="poolPingQuery" value="select 1 from abc"/>
<property name="poolPingConnectionsNotUsedFor" value="0"/>
</dataSource>
</environment>
@Test
public void ping() {
SqlSession sqlSession = sqlSessionFactory.openSession();
Student student = sqlSession.selectOne("selectByPrimaryKey", 2);
System.out.println("student = " + student);
sqlSession.close();
}
意外的是,多次执行会出现两种不一样的结果:
情况一:从数据库连接池中多次检出连接失败(每次都 ping 失败了,超过坏连接容忍阈值),直接报错。
2021-08-24 23:47:24,858 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - PooledDataSource: Could not get a good connection to the database.
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.sql.SQLException: PooledDataSource: Could not get a good connection to the database.
### The error may exist in com/sumkor/mapper/StudentMapper.java (best guess)
### The error may involve com.sumkor.mapper.StudentMapper.selectByPrimaryKey
### The error occurred while executing a query
### Cause: java.sql.SQLException: PooledDataSource: Could not get a good connection to the database.
情况二:第一次从连接池中拿到连接,ping 失败了,当作坏连接而作废掉,重新建立新连接,再校验新连接是否有效。注意,这里对新连接 ping 检查通过,因此正常执行完查询语句。
2021-08-25 00:13:17,830 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
2021-08-25 00:13:18,487 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 112797691.
2021-08-25 00:13:18,487 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Testing connection 112797691 ...
2021-08-25 00:13:18,516 [main] WARN [org.apache.ibatis.datasource.pooled.PooledDataSource] - Execution of ping query 'select 1 from abc' failed: Table 'testdb.abc' doesn't exist
2021-08-25 00:13:18,523 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Connection 112797691 is BAD: Table 'testdb.abc' doesn't exist
2021-08-25 00:13:18,523 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - A bad connection (112797691) was returned from the pool, getting another connection.
2021-08-25 00:13:18,550 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 112049309.
2021-08-25 00:13:18,550 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6adbc9d]
2021-08-25 00:13:18,561 [main] DEBUG [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - ==> Preparing: SELECT * FROM student WHERE id = ?
2021-08-25 00:13:18,603 [main] DEBUG [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - ==> Parameters: 2(Integer)
2021-08-25 00:13:18,638 [main] TRACE [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - <== Columns: id, name, phone, email, sex, locked, gmt_created, gmt_modified, delete
2021-08-25 00:13:18,640 [main] TRACE [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - <== Row: 2, 大明, 13821378271, xiaoli@mybatis.cn, 0, 0, 2018-08-30 18:27:42, 2018-10-08 20:54:29, null
2021-08-25 00:13:18,643 [main] DEBUG [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - <== Total: 1
student = Student{id=2, name='大明'}
2021-08-25 00:13:18,643 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6adbc9d]
2021-08-25 00:13:18,644 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6adbc9d]
2021-08-25 00:13:18,644 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Testing connection 112049309 ...
2021-08-25 00:13:18,645 [main] WARN [org.apache.ibatis.datasource.pooled.PooledDataSource] - Execution of ping query 'select 1 from abc' failed: Table 'testdb.abc' doesn't exist
2021-08-25 00:13:18,645 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Connection 112049309 is BAD: Table 'testdb.abc' doesn't exist
2021-08-25 00:13:18,645 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - A bad connection (112049309) attempted to return to the pool, discarding connection.
为什么会出现两种截然不同的结果呢?
注意到 MyBatis 的连接池向数据库发送 ping 检查的条件:
org.apache.ibatis.datasource.pooled.PooledDataSource#pingConnection
result && poolPingEnabled && poolPingConnectionsNotUsedFor >= 0
&& conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor
其中 conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor
限制了 当距离上一次使用连接的时间大于连接检查频率,才会发送 ping 检查。
org.apache.ibatis.datasource.pooled.PooledConnection#getTimeElapsedSinceLastUse
public long getTimeElapsedSinceLastUse() {
return System.currentTimeMillis() - lastUsedTimestamp;
}
本例中,新建的连接的上一次使用连接的时间 lastUsedTimestamp
就是该连接的创建时间。
只要该连接的创建时间与检查时间发生在同一毫秒内,System.currentTimeMillis() - lastUsedTimestamp
的计算结果就为 0,因此 conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor
得到为 false,这样就不会向数据库发送 ping 检查了。
由于创建时间与检查时间可能发生在同一毫秒内,也可能不在同一毫秒(取决于机器性能、是否打了断点),因此会出现两种不同的执行结果。
一般来说,只要配置了正确的 ping 语句,向数据库建立连接之后的同一毫秒内,就没有必要再检查连接了。
3.3 检出超时验证
设置数据库连接池的活跃连接集合大小为 1,每次只允许一个线程使用数据库连接。
设置数据库连接的最大检出时间为 1 秒,从连接池取出连接超过 1 秒没有归还,则认为检出超时。
开启两个线程,先后获取该连接,观察结果。
/**
* 验证检出超时
*/
@Test
public void timeout() throws InterruptedException {
Configuration configuration = sqlSessionFactory.getConfiguration();
Environment environment = configuration.getEnvironment();
PooledDataSource pooledDataSource = (PooledDataSource) environment.getDataSource();
System.out.println("pooledDataSource = " + pooledDataSource);
pooledDataSource.setPoolMaximumActiveConnections(1); // 活跃连接集合的容量为1
pooledDataSource.setPoolMaximumCheckoutTime(1000); // 最大检出时间为1秒
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(2);
// 线程一,检出,很久不归还
Thread thread01 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " start to open session...");
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
Student student = sqlSession.selectOne("selectByPrimaryKey", 1);
System.out.println("student = " + student);
// 休眠5秒之后,才让线程二获取连接
Thread.sleep(5000);
startLatch.countDown();
// 继续休眠1秒,再获取连接,发现被线程二设为已失效
Thread.sleep(1000);
sqlSession.getConnection().isValid(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
endLatch.countDown();
}
}
}, "thread_01");
// 线程二,在线程一检出过一段时间之后,再检出
Thread thread02 = new Thread(new Runnable() {
@Override
public void run() {
SqlSession sqlSession = null;
try {
startLatch.await();
System.out.println(Thread.currentThread().getName() + " start to open session...");
sqlSession = sqlSessionFactory.openSession();
Student student = sqlSession.selectOne("selectByPrimaryKey", 2);
System.out.println("student = " + student);
// 此时空闲连接集合为空,且活跃连接集合已满,则需要判读活跃连接集合中的连接,是否检出超时:
// 1. 超时,作废该连接;
// 2. 未超时,等待释放
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
endLatch.countDown();
}
}
}, "thread_02");
thread01.start();
thread02.start();
endLatch.await();
}
可以看到:
- thread_02 在 thread_01 检出数据库连接过一段时间之后才从连接池中获取连接,发现活跃连接集合中唯一的连接已经检出超时。
- 因此,thread_02 会作废当前的 PooledConnection,将原始的 ConnectionImpl 封装为新的 PooledConnection 去使用,完成数据库查询。
- 在此之后,thread_01 想继续操作原来的 PooledConnection,发现已经被 thread_02 设为无效,继而抛出异常
Connection is invalid
。
执行结果如下:
thread_01 start to open session...
2021-08-24 22:48:31,959 [thread_01] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
2021-08-24 22:48:32,306 [thread_01] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 212273522.
2021-08-24 22:48:32,306 [thread_01] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@ca70972]
2021-08-24 22:48:32,310 [thread_01] DEBUG [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - ==> Preparing: SELECT * FROM student WHERE id = ?
2021-08-24 22:48:32,361 [thread_01] DEBUG [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - ==> Parameters: 1(Integer)
2021-08-24 22:48:32,400 [thread_01] TRACE [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - <== Columns: id, name, phone, email, sex, locked, gmt_created, gmt_modified, delete
2021-08-24 22:48:32,401 [thread_01] TRACE [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - <== Row: 1, 小明, 13821378270, xiaoming@mybatis.cn, 1, 0, 2018-08-29 18:27:42, 2018-10-08 20:54:25, null
2021-08-24 22:48:32,408 [thread_01] DEBUG [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - <== Total: 1
student = Student{id=1, name='小明'}
thread_02 start to open session...
2021-08-24 22:48:37,413 [thread_02] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
2021-08-24 22:48:37,414 [thread_02] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Claimed overdue connection 212273522.
2021-08-24 22:48:37,414 [thread_02] DEBUG [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - ==> Preparing: SELECT * FROM student WHERE id = ?
2021-08-24 22:48:37,415 [thread_02] DEBUG [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - ==> Parameters: 2(Integer)
2021-08-24 22:48:37,416 [thread_02] TRACE [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - <== Columns: id, name, phone, email, sex, locked, gmt_created, gmt_modified, delete
2021-08-24 22:48:37,416 [thread_02] TRACE [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - <== Row: 2, 大明, 13821378271, xiaoli@mybatis.cn, 0, 0, 2018-08-30 18:27:42, 2018-10-08 20:54:29, null
2021-08-24 22:48:37,417 [thread_02] DEBUG [com.sumkor.mapper.StudentMapper.selectByPrimaryKey] - <== Total: 1
student = Student{id=2, name='大明'}
2021-08-24 22:48:37,418 [thread_02] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@ca70972]
2021-08-24 22:48:37,418 [thread_02] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@ca70972]
2021-08-24 22:48:37,419 [thread_02] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Returned connection 212273522 to pool.
2021-08-24 22:48:38,430 [thread_01] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Error resetting autocommit to true before closing the connection. Cause: java.sql.SQLException: Error accessing PooledConnection. Connection is invalid.
2021-08-24 22:48:38,430 [thread_01] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@ca70972]
2021-08-24 22:48:38,430 [thread_01] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - A bad connection (212273522) attempted to return to the pool, discarding connection.
java.sql.SQLException: Error accessing PooledConnection. Connection is invalid.
4. 总结
- 在 MyBatis 中,使用 PooledDataSource 数据源作为连接池对象,在连接池中存储的是 PooledConnection 对象。
- PooledConnection 对象会为原始连接对象,如 com.mysql.cj.jdbc.ConnectionImpl,生成动态代理。
- 每次从连接池中获取到的连接,实际上是一个代理对象。当代理对象归还连接池之后,会为原始连接对象生成新的代理对象,以供下次使用。而旧的代理对象会设为失效,无法继续使用。
- 通过这种方式,将代理的连接对象的生命周期限制在 SqlSession 范围之内,保证在会话关闭之后,不会有多个线程操作同一个数据库连接的问题。而原始的连接对象,可以在连接池中多次复用,避免反复向数据库建立连接。
- 当从连接池中获取、归还线程,都需要获取 synchronized 锁,由此做到线程安全。
作者:Sumkor
链接:https://segmentfault.com/a/11...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。