MyBatis原理——DataSource分析及数据库连接池的实现

前言

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

非池化版本比较简单,和传统通过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()方法是断开连接)。来达到”不断开,仅放回“的功能。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MyBatis是一个持久层框架,它并不直接提供数据库连接池的功能,而是依赖于其他的数据库连接池实现来管理数据库连接。 在MyBatis中,可以通过配置文件来指定使用的数据库连接池。常用的数据库连接池实现有: 1. Apache Commons DBCP:一个开源的数据库连接池实现,具有良好的性能和稳定性。 2. C3P0:另一个常用的开源数据库连接池实现,提供了更多的配置选项和监控功能。 3. HikariCP:一个高性能的数据库连接池实现,相对于DBCP和C3P0更为轻量级和快速。 你可以根据自己的需求选择适合的数据库连接池实现,并在MyBatis的配置文件中进行相关配置。一般情况下,你需要配置连接池的最大连接数、最小连接数、连接超时时间等参数。 例如,使用Apache Commons DBCP作为数据库连接池的示例配置如下: ```xml <dataSource type="org.apache.commons.dbcp2.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mydatabase"/> <property name="username" value="root"/> <property name="password" value="mypassword"/> <property name="initialSize" value="5"/> <property name="maxTotal" value="20"/> <property name="maxIdle" value="10"/> <property name="maxWaitMillis" value="10000"/> </dataSource> ``` 这是一个简单的配置示例,你可以根据自己的实际情况进行调整。在配置文件中,还可以设置其他属性,如连接池的验证语句、连接池的空闲对象清理策略等。 需要注意的是,MyBatis只是使用数据库连接池来管理数据库连接,并不负责连接池的具体实现。因此,你需要在项目中引入相应的数据库连接池实现,并将其配置到MyBatis的配置文件中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值