在 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

在 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 的行为:

  1. 把连接关闭行为 Connection#close,改为将连接归还连接池。
  2. 每次使用连接之前,检查 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 语句。

  1. 数据库连接未关闭。
  2. 在 MyBatis XML 中配置 poolPingEnabled 为 true。
  3. 距离上一次使用连接的时间,大于连接检查频率。

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

从池中取出数据库连接,代码流程:

  1. 采用 while 循环从数据库连接池中取出连接(该操作称为检出 checkout),每次循环开始都需要获取 PoolState state 对象锁。
  2. 检测 PoolState 中的空闲连接集合和活动连接集合,并从中获取连接对象,分为几种情况:
    2.1 空闲连接集合非空,则从中取出一个连接。
    2.2 空闲连接集合为空,活动连接集合未满,则利用数据库驱动包建立新连接,并包装为 PooledConnection 对象(生成动态代理)。
    2.3 空闲连接集合为空,活动连接集合已满,则需要对最早的连接进行检查:

     2.3.1 如果该连接已超时(代理对象的检出时间大于 poolMaximumCheckoutTime,但是原始连接可能还存活),此时将代理对象 PooledConnection 标记为失效,将原始连接封装为新的 PooledConnection 对象。
     2.3.2 如果该连接未超时,则当前的检出线程进入等待。
  3. 来到这一步,说明从 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

将连接归还数据库连接池。

代码流程:

  1. 获取 PoolState 对象锁。
  2. 从活跃连接集合中移除 PooledConnection,并检查连接是否有效。
  3. 若连接有效,则判断空闲集合是否已满:
    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 的使用流程如下:

  1. 解析 mybatis-config.xml 配置文件时,创建 PooledDataSource 连接池对象。
  2. 开启 SqlSession 数据库会话时,创建 JdbcTransaction 事务对象,利用 JdbcTransaction 来维护对数据库连接池的存取操作。
  3. 在一次会话中,JdbcTransaction 只会向连接池获取一个连接。在该会话范围之内,读写数据库的操作都通过该连接来完成。
  4. 关闭 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 继承体系:

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 使用之前,连接有效性的检查

从源码阅读可知,在使用数据库连接之前,有两个位置会检查连接的有效性:

  1. 从连接池中检出数据库连接,会调用 PooledConnection#isValid 方法检查连接是否有效,此时会向数据库发送 ping 语句检查原始连接是否有效。

org.apache.ibatis.datasource.pooled.PooledConnection#isValid

  public boolean isValid() {
    return valid && realConnection != null && dataSource.pingConnection(this);
  }
  1. 对于已经检出的连接,每次使用前会检查 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();
}

可以看到:

  1. thread_02 在 thread_01 检出数据库连接过一段时间之后才从连接池中获取连接,发现活跃连接集合中唯一的连接已经检出超时。
  2. 因此,thread_02 会作废当前的 PooledConnection,将原始的 ConnectionImpl 封装为新的 PooledConnection 去使用,完成数据库查询。
  3. 在此之后,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. 总结

  1. 在 MyBatis 中,使用 PooledDataSource 数据源作为连接池对象,在连接池中存储的是 PooledConnection 对象。
  2. PooledConnection 对象会为原始连接对象,如 com.mysql.cj.jdbc.ConnectionImpl,生成动态代理。
  3. 每次从连接池中获取到的连接,实际上是一个代理对象。当代理对象归还连接池之后,会为原始连接对象生成新的代理对象,以供下次使用。而旧的代理对象会设为失效,无法继续使用。
  4. 通过这种方式,将代理的连接对象的生命周期限制在 SqlSession 范围之内,保证在会话关闭之后,不会有多个线程操作同一个数据库连接的问题。而原始的连接对象,可以在连接池中多次复用,避免反复向数据库建立连接。
  5. 当从连接池中获取、归还线程,都需要获取 synchronized 锁,由此做到线程安全。

作者:Sumkor
链接:https://segmentfault.com/a/11...


Sumkor
148 声望1.3k 粉丝

会写点代码