mybatis源碼深度解析之dataSource

要執(zhí)行sql語句就要有db連接,mybatis的db連接也從DataSource獲取,mybatis實現(xiàn)了UNPOOLED和POOLED兩類數(shù)據(jù)源。

一、DataSorce的創(chuàng)建和使用

在解析配置文件是時根據(jù)指定的配置生產(chǎn)對應(yīng)的dataSorce,生成的dataSorce保存在configuration的environment中。配置文件被解析為cconfiguration,每個DataSourceFactory都通過其唯一的Cconfiguration來創(chuàng)建的,environment就是配置文件中的environments配置。

// src/main/java/org/apache/ibatis/session/Configuration.java
public class Configuration {
     protected Environment environment;
}

// src/main/java/org/apache/ibatis/mapping/Environment.java
public final class Environment {
    private final DataSource dataSource;
}

1.1 DataSorce實例生成

environments配置中的dataSource的type屬性指定了生成其dataSource實例的工廠類DataSourceFactory,通過對應(yīng)的DataSourceFactory來生產(chǎn)特定的DataSource。
DataSourceFactory接口只有兩個方法:

// src/main/java/org/apache/ibatis/datasource/DataSourceFactory.java
public interface DataSourceFactory {
    // 設(shè)置DataSource的屬性,配置文件中的屬性便是通過該方法設(shè)置的
    void setProperties(Properties props);
    // 獲取一個DataSource
    DataSource getDataSource();
}

在解析配置時根據(jù)的配置的type屬性DataSourceFactory類型并生成其實例。

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type");
    // 獲取dataSource配置的屬性
    Properties props = context.getChildrenAsProperties();
    // 創(chuàng)建DataSourceFactory
    DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    // 為DataSourceFactory的DataSource設(shè)置屬性
    factory.setProperties(props);
    return factory;
  }
  throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}

每個SqlSession都有一個用來執(zhí)行sql的執(zhí)行器Executor,Executor的事務(wù)管理器JdbcTransaction會持用Environment中的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);
      // 生成事務(wù)管理器并將dataSource給到事務(wù)管理器
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 生成sqlSession的事務(wù)管理器
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    }
}

在事務(wù)管理器中定義了獲取db連接的接口,mybatis提供的兩個事務(wù)管理器JDBC和MANAGED,兩個類型對該接口的實現(xiàn)方式都差不多,都是在getConnection是判斷其connection屬性是否為null,若不為null則通過其dataSource獲取到一個并給到connection屬性。

// src/main/java/org/apache/ibatis/transaction/Transaction.java
public interface Transaction {
    //獲取db連接
    Connection getConnection() throws SQLException;
}

// 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();
     // ...
    }
}

1.2 什么時候會獲取連接

在真正要對db執(zhí)行一臺語句的時候會去從事務(wù)管理器中去獲取連接,來生成執(zhí)行sql的Statement。
sqlSesson中的語句是通過其Exector來執(zhí)行的,在各個類型的Executor中,對于所以的語句只要不是從緩存可以獲取的或是語句的Statement有緩存,都會先從事務(wù)管理其中獲取到連接去生產(chǎn)執(zhí)行語句的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;
  }
}

1.3 什么時候釋放連接

連接的釋放依賴sqlSesson的關(guān)閉,也就是在關(guān)閉sqlSession時關(guān)閉連接。
連接是一個緊缺而重要的資源并且sqlSession也不是線程安全的,所以sqlSession在使用完成后要及時的關(guān)閉。mybatis官方甚至建議sqlSesion的生命周期應(yīng)該是請求內(nèi)的。

二、unpooled數(shù)據(jù)源

mybatis提供了unpooled、POOLED和JNDI三種類型的數(shù)據(jù)源:

  • unpooled:每次想數(shù)據(jù)源獲取數(shù)據(jù)時都會新建一個連接,在關(guān)閉是也會真正的將連接關(guān)閉:
  • POOLED:池化的數(shù)據(jù)源
  • JNDI:使用外部提供的數(shù)據(jù)源。

unpooled比較直接簡單,每次獲取連接時都新建一條連接,關(guān)閉sqlSesion時直接將其關(guān)閉。

// 獲取連接
private Connection doGetConnection(Properties properties) throws SQLException {
  initializeDriver();
  Connection connection = DriverManager.getConnection(url, properties);
  configureConnection(connection);
  return connection;
}

三、POOLED數(shù)據(jù)源

pooled數(shù)據(jù)源是池化的連接。

3.1 POOLEN連接池配置項

poolMaximumActiveConnections

最大活動連接數(shù),即被某個線程占用在使用的連接,默認(rèn)為10。

poolMaximumIdleConnections

最大空間連接數(shù),即未被任何線程使用的連接,默認(rèn)為5。

poolMaximumCheckoutTime

活躍連接被收回給其它線程使用前的等待時間,即活躍連接超過了該時間則有可能被給到其它線程使用,單位為毫秒,默認(rèn)為20000。

poolTimeToWait

當(dāng)需要連接到空閑連接為空同時活躍連接也到達(dá)了最大并且做‘老’的活躍連接也未超時時,獲取連接的動作要等待poolTimeToWait時間后再重新嘗試獲取,單位為毫秒,默認(rèn)為20000。

poolMaximumLocalBadConnectionTolerance

獲取到連接可能不能用了,這就是一個環(huán)連接,但獲取到一個環(huán)連接時會嘗試從新獲取,但一個線程一次獲取連接時獲取到的壞連接次數(shù)不能超過poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance,默認(rèn)為3。

poolPingEnabled

檢測連接是否可用時,是否開啟ping測試,這里ping測試是指通過使用該連接執(zhí)行一個sql看是否可用成功執(zhí)行。若執(zhí)行成功則連接可用,若執(zhí)行拋出任何異常連接都不可用。默認(rèn)為“NO PING QUERY SET”,執(zhí)行該語句一定會報錯,所以該配置必須配。

poolPingQuery

執(zhí)行ping測試時執(zhí)行的sql,默認(rèn)為false。

poolPingConnectionsNotUsedFor
poolPingConnectionsNotUsedFor執(zhí)行ping測試頻率,默認(rèn)為0即每次測試連接連接性的同時都進(jìn)行ping測試。

3.2 POOLED數(shù)據(jù)源簡介

POOLED數(shù)據(jù)源的連接池又分為空閑連接池和活動連接池,初始時這兩個池都是空的。獲取連接的基本步驟如下:

  1. 先從空閑連接池中獲取,若空閑連接池中有空閑的連接則返回。
  2. 若空閑連接池中沒有連接但活動連接池沒滿,則新建一個新的連接并放到活躍連接池中然后放回。
  3. 若空閑連接池中為空且活躍池也滿了,則嘗試從活躍連接池檢出第一個連接,當(dāng)然該連接可能當(dāng)前并沒有過期(沒有超過poolMaximumCheckoutTime),那么就等待poolTimeToWait時間后重復(fù)到第一步。
1598447976325-0e237a93bd2c040b.png

當(dāng)關(guān)閉連接時,對POOLED的連接并不會釋放,而是放回到空閑鏈接池中。

我們可以看到POOLED數(shù)據(jù)源的的瓶頸主要還是在于活躍連接池的大小,活躍連接池的數(shù)量決定了當(dāng)前最多有多少連接可以在同一時段內(nèi)訪問db,對于db訪問密集的應(yīng)用來說該數(shù)量甚至決定了單個應(yīng)用能同時支持的并發(fā)數(shù)。

同時db是在用過后才會放到空閑連接池中,并不會預(yù)先創(chuàng)建,這樣避免了持有過多無用的連接,但這要求我們連接要避免慢查詢并且一定要及時關(guān)閉連接。
上面只是連接池的基本邏輯,詳細(xì)實現(xiàn)如下。

3.3 PoolState

PoolState記錄了池的狀態(tài),主要有以下屬性:

  • accumulatedRequestTime:獲取鏈接累計耗時
  • requestCount: 向鏈接池請求獲取鏈接的累次次數(shù)
  • badConnectionCount:累計獲取到的壞鏈接數(shù)
  • claimedOverdueConnectionCount :從活躍鏈接池中累計獲取鏈接的數(shù)
  • hadToWaitCount:等待連接過期的等待次數(shù) 活躍鏈接被從獲取鏈接池中移除時累計存在的時間
  • idleConnections:空閑連接池
protected final List<PooledConnection> idleConnections = new ArrayList<>();
  • activeConnections:活躍連接池
protected final List<PooledConnection> activeConnections = new ArrayList<>();

3.5 PooledConnection

從POOLED的數(shù)據(jù)源中獲取到的連接都是Connection的動態(tài)代理實例,動態(tài)代理類就是PooledConnection,但與普通的動態(tài)代理不同的是每個Connection的動態(tài)代理實例都有一個唯一的動態(tài)代理類PooledConnection。

為什么要動態(tài)代理?
線程從POOLED中獲取到的連接后,可以對鏈接執(zhí)行任意的操作包括關(guān)閉鏈接的操作,但我們池化鏈接要是讓外部隨意就把鏈接關(guān)了,那么我們池中的鏈接可能慢慢的就都沒了,
所以不能讓連接被隨便的關(guān)閉,對于關(guān)閉的鏈接應(yīng)該放到空閑鏈接池中去給后續(xù)其它線程使用。
所以使用動態(tài)代理,PooledConnection代理類對于close方法并不會執(zhí)行colse而是將鏈接返回到空閑鏈接池中。

同時PooledConnection還記錄了連接的一些其它屬性:

  • checkoutTimestamp:鏈接被從池中檢出的時間,也就是從POOLED數(shù)據(jù)源中被獲取到的時間,但檢測連接是否到達(dá)從獲取連接池檢出時間時就是比較的該時間。
  • lastUsedTimestamp:鏈接最近被使用的時間,使用完后放回到空閑池的鏈接仍然是使用之前的lastUsedTimestamp時間。
  • createdTimestamp:PooledConnection的創(chuàng)建時間
  • realConnection:真正的鏈接
  • proxyConnection:connection的動態(tài)代理實例。
  • valid:鏈接是否還有效。
3.5.1 代理執(zhí)行

在創(chuàng)建PooledConnection是為其Connection創(chuàng)建一個動態(tài)代理實例,返回給外部的也正是這個動態(tài)代理的實例:

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;
  // 動態(tài)代理實例
  this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}

在執(zhí)行時若是執(zhí)行的是close方法會建連接返回到連接池中,若是是Object的方法則直接執(zhí)行,若執(zhí)行的是非close和非Object中的方法,則會先驗證valid是否為true,也就是連接是否有效:

@Override
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中繼承的發(fā)放先驗證vaild
    if (!Object.class.equals(method.getDeclaringClass())) {
      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.");
  }
}
3.5.2 判斷鏈接是否可用

在池中的鏈接會被一直持有者,也就是一直不會主動關(guān)閉,但難免會誤關(guān)閉或遇到一下特殊情況而“被”關(guān)閉導(dǎo)致的鏈接不可用的情況,例如mysql的鏈接在wait_timeout(默認(rèn)8小時)內(nèi)沒有與db發(fā)生交互那么mysql就會將鏈接置為過期,需要重新鏈接。所以判斷一個PooledConnection是真實可用的十分重要。
判斷PooledConnection是可用的首先valid必須是true并且realConnection不為null以及realConnection是可以‘ping’通的。

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

3.6 PooledDataSource

POOLED數(shù)據(jù)源的連接池的DataSource,實現(xiàn)了DataSource接口。用于獲取和管理池化的連接。POOLED數(shù)據(jù)源的幾個配置最終就是影射到它的幾個字段上,并根據(jù)這幾個配置來管理池化連接。此外還有一個PoolState字段保存了連接池狀態(tài)。

public class PooledDataSource implements DataSource {
    // 連接池狀態(tài)
    private final PoolState state = new PoolState(this);
    // 在任意時間可存在的活動(正在使用)連接數(shù)量
    protected int poolMaximumActiveConnections = 10;
    // 任意時間可能存在的空閑連接數(shù)
    protected int poolMaximumIdleConnections = 5;
    // 一個線程獲取連接時能容忍的連線獲取到壞連接數(shù)
    protected int poolMaximumLocalBadConnectionTolerance = 3;
    // 從active池中強制檢出的最大時間
    protected int poolMaximumCheckoutTime = 20000;
    // 沒有可用連接時再次嘗試獲取連接的等待時間
    protected int poolTimeToWait = 20000;
    // 是否進(jìn)行ping檢測
    protected boolean poolPingEnabled;
    // ping的頻率,默認(rèn)為0
    protected int poolPingConnectionsNotUsedFor;
    // ping檢測語句
    protected String poolPingQuery = "NO PING QUERY SET";
}

每個SqlSessionFactory只有一個連接池即只會有一個PooledDataSource,但從中獲取連接的線程有多個,為了保證獲取連接是的線程安全,在PooledDataSource中獲取連接和將連接返回到空閑池時的臨界區(qū)使用synchronized對state加鎖來同步訪問。

// 獲取連接
private PooledConnection popConnection(String userName, String password) throws SQLException {
    // ...
    while (conn == null) {
        synchronized (state) {
            // ...
        }
    }
}
// 連接放回空閑池
protected void pushConnection(PooledConnection conn) throws SQLException {
    synchronized (state) {
        // ...
    }
}
// 關(guān)閉所有連接
public void forceCloseAll() {
  synchronized (state) {
  }
}

3.6.1 pingConnection

PooledDataSource提供了ping檢測連接可用性。
pingConnection的ping探測不是我們常見的ping命令的探測,畢竟可ping通一個機(jī)器和這個機(jī)器的db鏈接可用可以說是是兩回事。
PooledDataSource的pingConnection先校驗連接是否已經(jīng)關(guān)閉了若已經(jīng)關(guān)閉了那么這個連接自然也就不能用了。
若連接未關(guān)閉并開啟了ping測試即poolPingEnabled配置為true,那么就使用連接執(zhí)行poolPingQuery指定的語句,語句執(zhí)行成功則任務(wù)鏈接正常,但若執(zhí)行時拋出任何異常都認(rèn)為鏈接不可用。
poolPingQuery的默認(rèn)配置為“NO PING QUERY SET”,這個語句是不符合sql語法的,執(zhí)行這個語句會拋出MySQLSyntaxErrorException異常,在執(zhí)行語句時拋出異常會認(rèn)為鏈接不可用,所以一定要記得配置poolPingQuery。
通過執(zhí)行語句來做檢測的‘成本’是很高的,所以配置poolPingQuery應(yīng)該盡量簡單并且沒有很大的查詢返回。同時pingConnection是ping檢測只有在連接從PooledDataSource中檢出的時間大于poolPingConnectionsNotUsedFor的時間是才進(jìn)行ping, poolPingConnectionsNotUsedFor默認(rèn)為0,建議將poolPingConnectionsNotUsedFor設(shè)置為和數(shù)據(jù)庫鏈接超時一樣避免不必要的檢測。
在進(jìn)行ping檢測是若檢測到鏈接不可用了,還會主動將鏈接close,當(dāng)再次但檢測這個鏈接時直接判斷closed而返回不用做sql執(zhí)行檢測。

protected boolean pingConnection(PooledConnection conn) {
    boolean result = true;
    // 已經(jīng)closed了那么必然是不可用了
    try {
        result = !conn.getRealConnection().isClosed();
    } catch (SQLException e) {
        return false;
    }

    // 啟用了ping且拼間隔時間大于等于0,同時返回使用的時間大于ping間隔時間則進(jìn)行檢測
    if (result && poolPingEnabled
            && poolPingConnectionsNotUsedFor >= 0
            && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
        try {
            Connection realConn = conn.getRealConnection();
            try (Statement statement = realConn.createStatement()) {
                statement.executeQuery(poolPingQuery).close();
            }
            if (!realConn.getAutoCommit()) {
                realConn.rollback();
            }
            result = true;
        } catch (Exception e) {
            try {
                // 對不可用的連接主動關(guān)閉
                conn.getRealConnection().close();
            } catch (Exception e2) {
            }
            result = false;
        }
    }
    return result;
}

什么時候進(jìn)行鏈接的有效性檢測?
只有以下兩個地方會進(jìn)行檢測:

  1. 從PooledDataSource獲取到鏈接時進(jìn)行檢測確保放回的鏈接是真的可用的。
private PooledConnection popConnection(String username, String password) throws SQLException {
    // 獲取連接
    ...
    // 獲取到連接對連接進(jìn)行檢測
    if (conn != null) {
        if (conn.isValid()) {
            // ...
        }
    }
}
  1. 還有就是在連接用完放回到空閑鏈接池的時候進(jìn)行檢測確保放回的連接是可用的。
protected void pushConnection(PooledConnection conn) throws SQLException {
    synchronized (state) {
        // 從活躍連接池中移除
        state.activeConnections.remove(conn);
        // 連接是可用的則返回空閑鏈接池
        if (conn.isValid()) {
            // 返回空閑連接池
        }
    }
}
3.6.2 獲取鏈接

PooledDataSource實現(xiàn)了DataSource接口,其中的getConnection兩個方法通過調(diào)用popConnection來獲取連接:

@Override
public Connection getConnection() throws SQLException {
  return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}

@Override
public Connection getConnection(String username, String password) throws SQLException {
  return popConnection(username, password).getProxyConnection();
}

popConnection獲取連接時經(jīng)過以下步驟:

1、若空閑連接池不為空則從空閑鏈接池只能夠獲取

if (!state.idleConnections.isEmpty()) {
  conn = state.idleConnections.remove(0);
}

2、如果空閑連接池為空但活躍連接池未滿則新建一個連接

if (state.activeConnections.size() < poolMaximumActiveConnections) {
    conn = new PooledConnection(dataSource.getConnection(), this);
    } else
}

3、若活躍連接池已滿則嘗試從活躍連接中檢出第一個,若第一個連接已經(jīng)過期則將其從活躍連接池中移除并使用其realConnection新建一個PoolenConnection,若連接未設(shè)置自動提交則執(zhí)行以下回滾,避免新連接給到其它線程使用是誤提交了非自己的操作,將舊的連接置為無效,這樣若其它線程還使用就的連接進(jìn)行操作就會拋出異常。

PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
    state.activeConnections.remove(oldestActiveConnection);
    if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
  try {
    oldestActiveConnection.getRealConnection().rollback();
  } catch (SQLException e) {
  }
  // 使用原來的連接建一個新的PooledConnection
  conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
  // 原來的連接置為無效
  oldestActiveConnection.invalidate();

}

4、 活躍鏈接池中的第一個連接可能并沒有過期,那么就要等待poolTimeToWait時間后從新進(jìn)行第一步

try {
  state.wait(poolTimeToWait);
} catch (InterruptedException e) {
  break;
}
``


##### 3.5.7 釋放連接
先將連接從活躍連接池中移除。再校驗連接是否是有效的若無效了則不會添加到空閑池中。
若連有效且空閑連接池未滿,則使用被關(guān)閉的連接的realConnection新建一個連接并添加到空閑連接池中,并將原來置為無效,防止再被原來持有該連接的線程使用到。

若空閑連接池已滿則直接將連接關(guān)閉,當(dāng)然關(guān)閉前還是會對非自動提交的連接主動進(jìn)行回滾。

```java
protected void pushConnection(PooledConnection conn) throws SQLException {

    synchronized (state) {
      // 將連接從活躍連接中移除
      state.activeConnections.remove(conn);
      if (conn.isValid()) {
        // 有效的連接才返回連接池
        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {
            // 非自動提交的主動回滾一下
            conn.getRealConnection().rollback();
          }
          // 使用原來的連接新建一個
          PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
          state.idleConnections.add(newConn);
          newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
          newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
          // 原來的連接置為無效
          conn.invalidate();
          state.notifyAll();
        }  
  }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容