目录

  • 前言
  • PooledDataSource
  • PoolState
  • PooledConnection
  • 总结

前言

前边《MyBatis原理——传统JDBC操作数据库》 提到,在MyBatis中,对于数据源DataSource有两个实现:非池化版本UnpooledDataSource和池化版本PooledDataSource

mybatic springboot 连接池 mybatis 连接池原理_数据库


非池化版本比较简单,和传统通过DriverManager获取数据库连接类似,只不过多了MyBatis的一些包装和校验。下边来记录下池化版本的实现。

PooledDataSource

简单看一下PooledDataSource的源码,这里只列出主要字段和构造函数:

public class PooledDataSource implements DataSource {
  // 保存所有活跃连接、空闲连接以及一些相关参数
  private final PoolState state = new PoolState(this);
  // 持有的非池化版本对象
  private final UnpooledDataSource dataSource;
  
  public PooledDataSource() {
      dataSource = new UnpooledDataSource();
  }

可以看到,其实PooledDataSource内部持有一个非池化版本的DataSource,它自己本身是不干什么实事的,具体真正去获取连接等细致的活还是交给UnpooledDataSource来做。只不过我们知道,数据库连接池主要的作用就是在:
1. 在获取连接的时候,如果连接池有空闲连接,就不去获取新的物理连接;
2. 释放连接的时候,并不关闭物理连接,而是放回连接池。

下面来看看MyBatis是如何解决这两个问题的:

PoolState

前边说到,PooledDataSource持有一个PoolState字段,这个才是真正保存连接的地方,这个类的主要字段如下:

public class PoolState {
  // 绑定的池化数据源
  protected PooledDataSource dataSource;
  // 空闲连接
  protected final List<PooledConnection> idleConnections = new ArrayList<>();
  // 活跃连接(正在工作的)
  protected final List<PooledConnection> activeConnections = new ArrayList<>();

PoolState主要就是有这两个List,一个保存空闲连接,一个保存活跃连接,此外还有一些相关的基本统计字段,比如记录请求数量、累计请求时间等等。PooledDataSource持有它,就可以根据已有配置和相关字段的统计信息,操作这两个List,达到连接池的功能。而列表中保存的PooledConnection则是MyBatis对基本数据库连接的封装。首先简单看下PooledDataSource是如何获取连接的:

@Override
  public Connection getConnection() throws SQLException {
    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
  }
  
  private PooledConnection popConnection(String username, String password) throws SQLException {
  
    PooledConnection conn = null;
    
    while (conn == null) {
      synchronized (state) {
        // 如果存在空闲连接,则直接从空闲连接列表中获取
        if (!state.idleConnections.isEmpty()) {
          // Pool has available connection
          conn = state.idleConnections.remove(0);
        } else {
          // 不存在空闲连接
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            // 创建新连接
            conn = new PooledConnection(dataSource.getConnection(), this);
          } else {
            // Cannot create new connection
			// ...
		  }
        }
        if (conn != null) {
			// 对连接做一些校验和配置
			// ...
			state.activeConnections.add(conn);
            state.requestCount++;
        }
      }
    }

    if (conn == null) {
      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }
    return conn;
  }

这个方法本身很长,删去了一些复杂情况,仅保留最简单的内容。可以看到其实就是对PoolState中两个列表的操作:

  1. 如果存在空闲连接,则直接从列表上取下来一个。
  2. 如果不存在空闲连接,但是当前活跃连接数小于配置的最大连接数,那么新建一个连接,新建连接从本类持有的非池化DataSource获取:dataSource.getConnection()。并放到池中(state.activeConnections.add(conn);)。这一点类似JDK的线程池,有常驻线程数,最大线程数等配置信息。

当然还有其他情况,比如达到最大连接数,不能再新建了,那么就需要根据PooledDataSourcePoolState中的相关配置、统计信息采取策略了,比如替换最老的连接。这里不再赘述。

可以看到,这里popConnection 返回的是PooledConnection对象,getConnection()返回的,则是PooledConnection..getProxyConnection();一个代理连接。所以最后的奥妙就在PooledConnection中。

PooledConnection

惯例先看下PooledConnection类的主要字段和构造方法:

class PooledConnection implements InvocationHandler {

  private static final String CLOSE = "close";
  private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };
  // 绑定的数据源
  private final PooledDataSource dataSource;
  // 真实数据库连接
  private final Connection realConnection;
  // 代理数据库连接
  private final Connection proxyConnection;
  
  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);
  }
  
  public Connection getProxyConnection() {
    return proxyConnection;
  }

可以看到PooledConnection其实持有“两个数据库连接”。在构造方法里,一个是真实的数据库连接,由构造方法的参数传递而来,该参数由PooledDataSource持有的UnpooledDataSource中获取。另一个则是对真实连接的包装(JDK动态代理),并将自己作为代理的实现者传递进去。紧接上边得知:最终是这个代理连接被从数据库连接池中取出,它实际承担了连接对象的责任。

PooledConnection本身实现了InvocationHandler接口,所以它对连接对象搞的鬼就都在实现的invoke方法中了:

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    if (CLOSE.hashCode() == methodName.hashCode() && 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);
    }
  }

代理方法意外的简单,大部分任务都直接委托给了真实的数据库连接(realConnection),仅在开头对close()方法做了拦截,如果当前调用的是close()方法,那么仅仅将本连接放入数据库连接池:dataSource.pushConnection(this);该push方法最终会将 连接再次放入到PoolState对象中。至此终于实现了连接池的功能。

总结

  1. MyBatis的池化数据源PooledDataSource持有真实数据源UnpooledDataSource和连接池容器PoolState对象,获取连接时从连接池容器中调度获取,而当需要获取真实物理连接时,仍然是委托UnpooledDataSource来做。
  2. PoolState持有空闲连接和活跃连接两个列表,根据配置参数和统计信息来供PooledDataSource使用。
  3. PooledConnection借助动态代理创建一个代理连接,并同时持有代理连接和真实连接两者,返回代理连接来执行具体数据库任务。
  4. 代理连接把大部分任务直接委托给了真实连接来做,而仅仅拦截close()方法,把自身放回到连接池容器(因为真实连接的close()方法是断开连接)。来达到”不断开,仅放回“的功能。