Mybatis源码深度解析之DataSource

2,185 阅读9分钟

通过java的jdbc对db执行一个sql的一般方式如下:

// 加载驱动,jdbc2后可以省略
Class.forName(driver);
// 获取db连接
Connection connection = DriverManager.getConnection(url, user, password);
// 创建Statement
Statement statement = connection.createStatement();
// 执行语句
statement.execute(sql1);

现在有了许多持久化框架,我们基本上不会再直接使用jdbc操作db了,但各个框架也是对jdbc的封装,要执行一个sql上面的步骤还是必不可少的,或许框架做了一下操作让我们不用感知到这些的过程。

获取db连接是执行sql的第一个也是基石,从 JDBC 2.0后java推荐使用DataSource来代表一个db数据源并从中获取与db的连接,DataSource是一个简单的接口只包含了两个文明获取连接的接口定义:

package javax.sql;
public interface DataSource  extends CommonDataSource, Wrapper {
// 获取一个与db的连接
Connection getConnection() throws SQLException;

// 获取一个与db的连接,可以指定获取连接时的用户和密码
Connection getConnection(String username, String password) throws SQLException;

mybatis提供了UNPOOLED和POOLED两类DataSource。 DataSource相关的代码在src/main/java/org/apache/ibatis/datasource下。

一、UnpooledDataSource

在配置文件中若配置dataSource的type未UNPOOLED,则使用的DataSource为UnpooledDataSource,每次从中获取连接时都会新建一个连接,在关闭是也会真正的关闭与db的连接。

UnpooledDataSource封装了获取db连接的一些属性,这些属性在解析配置的时候从配置文件中取得,在获取连接时通过这些属性值来取得连接。包括以下属性:

package org.apache.ibatis.datasource.unpooled;

public class UnpooledDataSource implements DataSource {
    // 驱动
    private String driver;
    // db连接url
    private String url;
    // 用户名
    private String username;
    // 密码
    private String password;
    // 其它连接时的驱动属性,通过driver.xxx配置的属性
    private Properties driverProperties;
    
    // 获取到的连接是否自动提交
    private Boolean autoCommit;
    // 连接默认的事务隔离级别
    private Integer defaultTransactionIsolationLevel;
    // 连接默认的执行超时
    private Integer defaultNetworkTimeout;
}

这些属性可以在mybatis文档中查到:mybatis.org/mybatis-3/z…

获取连接依然通过DriverManager.getConnection方法获取:

package org.apache.ibatis.datasource.unpooled;

public class UnpooledDataSource implements DataSource {
  // 获取连接
  private Connection doGetConnection(Properties properties) throws SQLException {
    initializeDriver();
    Connection connection = DriverManager.getConnection(url, properties);
    configureConnection(connection);
    return connection;
  }
  
  // 配置连接属性
  private void configureConnection(Connection conn) throws SQLException {
    if (defaultNetworkTimeout != null) {
      conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), defaultNetworkTimeout);
    }
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
      conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  }

}

获取连接前通过initializeDriver方法检查驱动是否已经加载,若未加载则使用指定的类型加载器或Class.forName来加载。
properties包含指定的user、password和通过driver.xxx指定的属性。
获取到连接后通过configureConnection方法设定连接的autoCommit、defaultTransactionIsolationLevel和defaultNetworkTimeout。

二、PooledDataSource

在配置文件中若配置dataSource的type未POOLED,则使用的DataSource为PooledDataSource,每次从中获取的连接都是池化管理的,连接在关闭是会返回到空闲池中并不会真正的关闭与db的连接。

PooledDataSource实现在src/main/java/org/apache/ibatis/datasource/pooled/PooledDataSource.java文件中。

2.1 创建db连接

PooledDataSource的连接是池化的,这些池中连接任是通过UnpooledDataSource创建的。PooledDataSource创建了一个UnpooledDataSource的实例,在需要创建新的连接时通过其创建连接。

package org.apache.ibatis.datasource.pooled;

public class PooledDataSource implements DataSource {
    // 池相关配置
    // 池中活跃连接最大数量
    protected int poolMaximumActiveConnections = 10;
    
    private final UnpooledDataSource dataSource;
    public PooledDataSource() {
        dataSource = new UnpooledDataSource();
    }
    // 设置连接相关属性时给到dataSource,其它属性的设置与url的设置相同
    public void setUrl(String url) {
        dataSource.setUrl(url);
        ...
    }
}

配置文件中driver、url、username、password、defaultTransactionIsolationLevel、defaultNetworkTimeout 这些与非连接池的属性也是给到UnpooledDataSource的实例。

2.2 PooledConnection

PooledDataSource中的连接是是池化的,这就要求在关闭连接时连接应该是被回收到池中,而不是真正的被关闭与db的连接并释放相关资源。所以再从UnpooledDataSource中获取到连接后,会为连接创建一个动态代理PooledConnection。

class PooledConnection implements InvocationHandler {
    // 动态代理只实现db连接Connection类
    private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };
    // 动态代理的连接
    private final Connection proxyConnection;
    
    private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };
    
    public PooledConnection(Connection connection, PooledDataSource dataSource) {
        ...
        // 创建连接的动态代理
        this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
  }
}

在PooledDataSource的代理方法invoke方法中,对于close的调用会将连接放回到空闲连接池中,而对于其它的非Object的方法的调用则回先检查连接是否可用。

package org.apache.ibatis.datasource.pooled;
class PooledConnection implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        // close方法将连接放回池中
        if (CLOSE.equals(methodName)) {
            dataSource.pushConnection(this);
            return null;
        }
        try {
           // 非Object的方法要先校验连接是否可用
           if (!Object.class.equals(method.getDeclaringClass())) {
               checkConnection();
           }
           // 执行方法
           return method.invoke(realConnection, args);
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
    }
}

PooledConnection除了动态代理外,还包括了一下连接的信息用于池化管理:

class PooledConnection implements InvocationHandler {
  // 原始的连接
  private final Connection realConnection;
  // 动态代理的连接
  private final Connection proxyConnection;
  // 从池中检出给其它线程使用的时间
  private long checkoutTimestamp;
  // 创建时间
  private long createdTimestamp;
  // 最后使用时间
  private long lastUsedTimestamp;
  // 
  private int connectionTypeCode;
  // 连接是否有效
  private boolean valid;
}

2.3 PoolState

PoolState记录的连接池的状态已经持有空闲连接和活跃连接(被某个线程使用者的连接),每个PooledDataSource都有一个该类的实例。同时PooledConnection的获取连接的操作是同步的,使用的PoolState实例作为同步锁。

package org.apache.ibatis.datasource.pooled;
public class PoolState {
  // 空闲连接池
  protected final List<PooledConnection> idleConnections = new ArrayList<>();
  // 活跃连接池
  protected final List<PooledConnection> activeConnections = new ArrayList<>();
}

使用state作同步锁

public class PooledDataSource implements DataSource {
    private final PoolState state = new PoolState(this);
    
    // 获取连接
    private PooledConnection popConnection(String username, String password) throws SQLException {
        while (conn == null) {
            // 同步操作
            synchronized (state) {
                ...
            }
        }
    }
}

2.4 从池中获取连接

PooledDataSource分为空闲连接池和活动连接池,初始时这两个池都是空的。获取连接的基本步骤如下:

  1. 若空闲连接池中有空闲的连接则从空闲连接池中取出第一个连接返回。
PooledConnection conn = null;
while (conn == null) {
  if (!state.idleConnections.isEmpty()) {
    // 从空闲池中获取
    conn = state.idleConnections.remove(0);    
  } 
 }
  1. 否则若活跃连接池没满,则新建一个新的连接。
if (state.activeConnections.size() < poolMaximumActiveConnections) {
    conn = new PooledConnection(dataSource.getConnection(), this);
}

  1. 否则尝试从活跃连接池检出第一个连接。
    若活跃连接池中的第一个连接从池中取出的时间间隔已经大于了最大检出时间poolMaximumCheckoutTime,这个连接可以强制返回并检出给其它线程使用:
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
// 检出时间间隔
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
// 大于最大被检出时间则检出
if (longestCheckoutTime > poolMaximumCheckoutTime) {
     state.activeConnections.remove(oldestActiveConnection);
}

但活跃连接池中的连接是还在被其它线程占用的连接,所以被检出的连接可能其它线程正常使用或后面会使用,所以对直接检出的连接要回滚一下并设置为无效,然后再用其真实连接新建一个PooledConnection。

// 未设置自动提交则回滚
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
    try {
       oldestActiveConnection.getRealConnection().rollback();
    } catch (SQLException e) {
       log.debug("Bad connection. Could not roll back");
    }
}

// 新建一个连接
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
// 旧连接设为无效
oldestActiveConnection.invalidate();

当然该连接可能当前并没有过期,那么就等待poolTimeToWait时间后重复到第一步。

state.wait(poolTimeToWait);

在经过上面步骤若获取到的连接若是一个无效的连接,那么当前获取到无效连接的数量加1,但若无效连接的数量超过poolMaximumIdleConnections+poolMaximumLocalBadConnectionTolerance时会直接抛出异常:

if (conn.isValid()) {

} else {
    localBadConnectionCount++;
    if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
       throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
   }
}

在经过上面步骤获取到有效的后连接将连接放到活跃连接池中并放回,整个过程如下:

1598447976325-0e237a93bd2c040b.png

2.4 关闭连接

PooledDataSource中获取到连接后会为连接创建一个动态代理PooledConnection,在PooledConnection代理执行时,若访问的是关闭连接的close方法那么会将放回到空闲池中:

// PooledConnectiond的动态代理
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    if (CLOSE.equals(methodName)) {
      dataSource.pushConnection(this);
      return null;
    }
}

放回空闲池时连接可能已经是无效的了,在下面的情况下会发生这种情况:

  1. 这个连接上传检出的时间已经超过poolTimeToWait,并且被其它线程从活跃池中检出,这时原线程在close者连接时这个连接就已经时无效的了。
  2. 修改了PooledDataSource连接的autoCommit、url、password这类连接相关的属性后PooledDataSource会将空闲池和活跃池中的所有连接都设为无效并关闭。

对于无效的连接是不会被放回空闲池的而是直接忽略。

// 将连接放回池中
protected void pushConnection(PooledConnection conn) throws SQLException {
    synchronized (state) {
        state.activeConnections.remove(conn);
    }
    if (conn.isValid()) {
        ....
    } else {
       // 无效连接关闭忽略
       state.badConnectionCount++;
    }
}

若连接是有效的并且空闲链接池未慢,就使用该连接的真实连接新建一个PooledConnection并放到空闲连接池中,然后将原连接置为无效。

if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
    // 若连接不是自动提交的就执行一下回滚
    if (!conn.getRealConnection().getAutoCommit()) {
        conn.getRealConnection().rollback();
    }
    // 新将一个连接并加到连接池中
    PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
    state.idleConnections.add(newConn);
    // 原连接置为无效
    conn.invalidate();
}

2.5 校验连接是否有效

从池中获取到的连接和放回到池中饿连接都要校验其是否是有效的连接,除了校验PooledConnection的valid标示外还进行ping测试,valid标示只能校验连接是否在池中有效,对于一下连接自身的的移除如连接已经被db端置为无效这类错误还是要通过ping检查才能发现。

class PooledConnection implements InvocationHandler {
  public boolean isValid() {
    return valid && realConnection != null && dataSource.pingConnection(this);
  }
}

在进行ping进程是先校验真正的Connection是否被关闭了:

protected boolean pingConnection(PooledConnection conn) {
    boolean result = true;
    try {
      result = !conn.getRealConnection().isClosed();
    } catch (SQLException e) {
      result = false;
    }
}

若连接未被关闭并且配置中开启了poolPingEnabled,同时该连接的最后使用时间到现在的时间间隔大于poolPingConnectionsNotUsedFor配置的时间那么就对db发起poolPingQuery配置的语句执行来测试,若执行语句没有抛出异常则连接可用:

if (result && poolPingEnabled && poolPingConnectionsNotUsedFor >= 0
        && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
    Connection realConn = conn.getRealConnection();
    try (Statement statement = realConn.createStatement()) {
        statement.executeQuery(poolPingQuery).close();
    }
    result = true;
}

三、DataSorce的创建

DataSorce在environment中配置,需要指定类型和各类配置参数,详细:mybatis.org/mybatis-3/z…

<environment id="development">
   <dataSource type="UNPOOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
</environment>

在使用SqlSessionFactoryBuilder的build方法创建SqlSessionFactory时,会通过src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java来解析配置文件,解析后各项配置和sql语句放到Configuration(org/apache/ibatis/session/Configuration.java)中。在解析时会解析dataSource配置项目,根据配置生成DataSorce。

在解析时通过type属性获取到类型对应的工厂类并创建实例,然后将property通过反射设置到DataSorce中:

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      // 创建工厂类实例
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
      // 设置属性
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }

PooledDataSourceFactory和UnpooledDataSourceFactory会直接new对应的DataSource。由工厂方法得到的DataSource放到Configuration的environment中,供后面执行语句时使用。

四、DataSorce的使用

4.1 Transaction提供连接

每个SqlSession都有一个用来执行sql的执行器Executor,每个Executor都有一个事务管理器transaction,transaction通过transactionManager配置生成,同时Transaction还会持有dataSource。

// src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSessionFactory.java
public class DefaultSqlSessionFactory implements SqlSessionFactory {
   
  // 生成SqlSession
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 生成事务管理器并将dataSource给到事务管理器
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 生成sqlSession的执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    }
}

SqlSession在执行的语句由executor负责执行。executor的事务管理器除了提供提交回滚的操作外还负责提供db连接和关闭db连接。下面是Transaction接口的定义:

// src/main/java/org/apache/ibatis/transaction/Transaction.java
public interface Transaction {
    // 获取只供当前executor使用的连接
    Connection getConnection() throws SQLException;
    // 关闭executor当前使用的连接
    void close() throws SQLException;
    
    // 其它事务相关的接口
    ...
}

mybatis提供类JDBC和MANAGED两类事务管理器,两个类型对该接口的实现方式都差不多,都是在getConnection是判断其connection属性是否为null,若不为null则通过其dataSource获取到一个并给到connection属性。


// src/main/java/org/apache/ibatis/transaction/jdbc/JdbcTransaction.java
public class JdbcTransaction implements Transaction {
    protected Connection connection;
    protected DataSource dataSource;
    
    // 连接存在直接返回连接
    @Override
    public Connection getConnection() throws SQLException {
      if (connection == null) {
        openConnection();
      }
      return connection;
    }
   
    // 从dataSource中获取连接
    protected void openConnection() throws SQLException {
        connection = dataSource.getConnection();
     // ...
    }
}

4.2 什么时候会获取连接

在真正要对db执行一台语句的时候会去从事务管理器中去获取连接,来生成执行sql的Statement。
在Executor中,对于所以的语句只要不是从缓存可以获取的或是语句的Statement有缓存,都会先从事务管理其中获取到连接去生产执行语句的Statement。

在所有Executor的父类BaseExecutor中定义了获取连接:

// src/main/java/org/apache/ibatis/executor/BatchExecutor.java
protected Connection getConnection(Log statementLog) throws SQLException {
  Connection connection = transaction.getConnection();
  if (statementLog.isDebugEnabled()) {
    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  } else {
    return connection;
  }
}

4.3 什么时候释放连接

连接的释放依赖sqlSesson的关闭,在关闭sqlSession时会关闭Executor,在Eecuto的close方法中会关闭连接。

public class DefaultSqlSession implements SqlSession {
    public void close() {
        try {
            executor.close(isCommitOrRollbackRequired(false));
            ...
        } finally {
           ...
        }
    }
}

public class JdbcTransaction implements Transaction {
  public void close() throws SQLException {
    if (connection != null) {
      resetAutoCommit();
      connection.close();
    }
  }
}

连接是一个紧缺而重要的资源并且sqlSession不是线程安全的,所以sqlSession在使用完成后要及时的关闭以释放其占用的连接。mybatis官方建议sqlSesion的生命周期应该是请求内的。