mybatis源碼分析03:執行器

注:本系列源碼分析基於mybatis 3.5.6,源碼的gitee倉庫倉庫地址:funcy/mybatis.

mybatis執行sql語句的操作是由執行器(Executor)完成的,mybatis中一共提供了3種Executor

類型 名稱 功能
REUSE 重用執行器 緩存PreparedStatement,下一次執行相同的sql可重用
BATCH 批量執行器 將修改操作記錄在本地,等待程序觸發或有下一次查詢時才批量執行修改操作
SIMPLE 簡單執行器 對每一次執行都生成PreparedStatement,執行完就關閉,不緩存

另外,mybatis 還提供了一個緩存執行器CachingExecutor,該執行器實際上是以上三種執行器的裝飾類,用以處理緩存相關操作,實際幹活的還是以上三種執行器之一。

Executor的繼續結構如下:

1. BaseExecutor

BaseExecutor實現了Executor的基本操作,如:

  • 事務的處理:
    • commit(...):處理事務的提交
    • rollback(...):處理事務的回滾
  • 緩存的處理:
    • createCacheKey(...):創建緩存key
    • clearLocalCache(...):清除緩存
  • curd操作:
    • query(...):查詢操作
    • update(...):更新操作,插入與刪除也是在這裏處理
  • 留待子類的實現
    • doUpdate(...):具體的更新操作,留待子類實現
    • doQuery(...):具體的查詢操作,留待子類實現

接下來我們關注Executor的實現時,只關注留待子類實現的方法。

2. SimpleExecutor

SimpleExecutor會對每一次執行都生成PreparedStatement,執行完就關閉,不緩存,我們來看看它是怎麼實現的,來看看它的doQuery(...)方法:

  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, 
        ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      // 獲取配置
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, 
            rowBounds, resultHandler, boundSql);
      // 得到 PrepareStatement
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 執行查詢
      return handler.query(stmt, resultHandler);
    } finally {
      // 關閉 Statement
      closeStatement(stmt);
    }
  }

獲取Statement的方法爲SimpleExecutor#prepareStatement

  private Statement prepareStatement(StatementHandler handler, Log statementLog) 
        throws SQLException {
    Statement stmt;
    // 獲取數據庫連接
    Connection connection = getConnection(statementLog);
    // 獲取 Statement
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 處理參數設置
    handler.parameterize(stmt);
    return stmt;
  }

這個方法先是獲取了數據庫連接,接着獲取Statement,然後處理了參數設置。

關於數據庫連接的獲取,我們在分析配置文件的解析時,數據源的配置最終會轉化成PooledDataSourceUnpooledDataSource對象,數據庫連接就是從數據源來的。

至於Statement的生成,PreparedStatement的實例化操作方法爲PreparedStatementHandler#instantiateStatement,這些都是常規的jdbc操作,就不細看了。

處理sql的執行方法爲PreparedStatementHandler#query

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 執行
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
  }

SimpleExecutor#doQuery(...)的執行流程如下:

  1. 獲取數據庫連接
  2. 獲取PrepareStatement
  3. 執行查詢
  4. 關閉PrepareStatement

SimpleExecutor的操作就是常規的jdbc操作。

3. ReuseExecutor

ReuseExecutor會緩存PreparedStatement,下一次執行相同的sql可重用。

我們依然分析doQuery(...)方法:

  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, 
        ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, 
            rowBounds, resultHandler, boundSql);
    // 獲取 Statement
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    // 處理查詢操作
    return handler.query(stmt, resultHandler);
  }

SimpleExecutor相比,ReuseExecutordoQuery(...)方法並沒關閉Statement.我們來看看Statement的獲取操作:

private Statement prepareStatement(StatementHandler handler, Log statementLog) 
        throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    // 根據sql語句判斷是否有Statement緩存
    if (hasStatementFor(sql)) {
      // 有緩存,直接使用
      stmt = getStatement(sql);
      applyTransactionTimeout(stmt);
    } else {
      // 沒緩存,獲取數據庫連接,再獲取 Statement
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      // 緩存 Statement
      putStatement(sql, stmt);
    }
    // 處理參數
    handler.parameterize(stmt);
    return stmt;
}

可以看到,ReuseExecutor獲取Statement時,會先從緩存裏獲取,緩存裏沒有才會新建一個Statement,然後將新建的Statement添加到緩存中。從這裏可以看出,ReuseExecutorReuse,複用的是Statement

我們再來看看緩存Statement的結構:

public class ReuseExecutor extends BaseExecutor {

  private final Map<String, Statement> statementMap = new HashMap<>();

  ...

  private Statement getStatement(String s) {
    return statementMap.get(s);
  }

  private void putStatement(String sql, Statement stmt) {
    statementMap.put(sql, stmt);
  }

}

由些可見,緩存Statement的是一個Mapkeysql語句,valueStatement.

4. BatchExecutor

BatchExecutor會將修改操作記錄在本地,等待程序觸發或有下一次查詢時才批量執行修改操作,即:

  1. 進行修改操作(insertupdatedelete)時,並不會立即執行,而是會緩存到本地
  2. 進行查詢操作(select)時,會先處理緩存到本地的修改操作,再進行查詢操作
  3. 也可行觸發修改操作

從以上內容來看,這種方式似乎有大坑,列舉幾點如下:

  1. 修改操作緩存到本地後,如果執行前遇到意外重啓,緩存的記錄會不會丟失?
  2. 分佈式環境下,多機共同協作,更新在A機上執行,查詢在B機上執行,B機是不是不能查到B機的更新記錄(B機的更新操作還在緩存中,並未執行)?

我們來看下BatchExecutor的更新操作,進入doUpdate(...)方法:

  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, 
            RowBounds.DEFAULT, null, null);
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
    // 如果傳入的sql是當前保存的 sql,直接使用
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      int last = statementList.size() - 1;
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
      handler.parameterize(stmt);// fix Issues 322
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      // 創建連接,獲取 Statement
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);    // fix Issues 322
      currentSql = sql;
      currentStatement = ms;
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    // 保存,等待之後批量執行
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }

BatchExecutor有成員變量會記錄上一次執行的sqlMappedStatement,如果本次執行的sqlMappedStatement與上一次執行的相同,則直接使用上一次的Statement,否則就新建連接、獲取Statement.

得到Statement後,會調用PreparedStatementHandler#batch方法:

  public void batch(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.addBatch();
  }

這個方法並沒有執行,只是調用PreparedStatement#addBatch方法,將當前statement保存了起來。

PreparedStatement#addBatch方法如何使用呢?簡單示意下:

// 獲取連接
Connection connection = getConnection();
// 預編譯sql
String sql = "xxx";
PreparedStatement statement = connection.prepareStatement(sql);   

//記錄1
statement.setInt(1, 1);
statement.setString(2, "one");
statement.addBatch();   

//記錄2
statement.setInt(1, 2);
statement.setString(2, "two");
statement.addBatch();   

//記錄3
statement.setInt(1, 3);
statement.setString(2, "three");
statement.addBatch();   

//批量執行
int[] counts = statement.executeBatch();

// 關閉statment,關閉連接
...

BatchExecutordoUpdate(...)方法並沒有執行sql語句,我們再來看看doQuery(...)方法:

  public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
        ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      // 處理緩存中的 statements
      flushStatements();
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, 
            rowBounds, resultHandler, boundSql);
      // 獲取連接,獲取Statement,處理參數
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);
      // 執行查詢
      return handler.query(stmt, resultHandler);
    } finally {
      // 關閉 Statement
      closeStatement(stmt);
    }
  }

doQuery(...)方法會先調用flushStatements()方法,然後再處理查詢操作,整個過程基本同SimpleExecutor一致,即"獲取數據庫連接-獲取Statement-處理查詢-關閉Statement"等幾步。我們重點來看flushStatements()方法的流程.

flushStatements()方法最終調用的是BatchExecutor#doFlushStatements方法,代碼如下:

  public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List<BatchResult> results = new ArrayList<>();
      if (isRollback) {
        return Collections.emptyList();
      }
      // 遍歷的statementList,statementList就是緩存statement的結構
      for (int i = 0, n = statementList.size(); i < n; i++) {
        Statement stmt = statementList.get(i);
        applyTransactionTimeout(stmt);
        BatchResult batchResult = batchResultList.get(i);
        try {
          // 關鍵代碼:stmt.executeBatch(),批量執行sql
          batchResult.setUpdateCounts(stmt.executeBatch());
          ...
        } catch (BatchUpdateException e) {
          ...
        }
        results.add(batchResult);
      }
      return results;
    } finally {
      ...
    }
  }

BatchExecutor#doFlushStatements方法的關鍵代碼就是batchResult.setUpdateCounts(stmt.executeBatch());了 ,其中的stmt.executeBatch()就是批量執行更新操作了。

從以上分析可知,BatchExecutor#doUpdate(...)方法不會執行sql語句,只是把sql語句轉換爲Statement然後緩存起來,在執行BatchExecutor#doQuery(...)方法時,會先執行緩存起來的Statement,然後再執行查詢操作,當然也可以手動調用BatchExecutor#flushStatements方法執行緩存的Statement

5. CachingExecutor

CachingExecutor不同於以上3種執行器,它是一個裝飾類,可以從緩存中獲取數據,實際幹活的還是以上三種執行器之一:

public class CachingExecutor implements Executor {

  // 具體的執行器
  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

  ...
}

從代碼來看,它是Executor的子類,其中有一個成員變量delegate,它的類型爲Executor,由構造方法傳入。也就是說,在創建CachingExecutor時,會傳入以上3種執行器之一,CachingExecutor會把它保存到成員變量delegate中。

CachingExecutorquery(...)方法如下:

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
        ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 創建緩存key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
        ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    // 操作緩存
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 從緩存中獲取  
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 實際處理查詢的操作  
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 添加到緩存中
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

從代碼上來看,CachingExecutor在處理查詢時,會先從緩存中獲取,當緩存中不存在時,就執行具體執行器的query(xxx)方法。


本文原文鏈接:https://my.oschina.net/funcy/blog/4952948 ,限於作者個人水平,文中難免有錯誤之處,歡迎指正!原創不易,商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章