天天看點

深入了解 Mybatis - Executor

承接上篇部落格, 本文探究MyBatis中的Executor, 如下圖: 是Executor體系圖

深入了解 Mybatis - Executor

本片部落格的目的就是探究如上圖中從頂級接口Executor中拓展出來的各個子執行器的功能,以及進一步了解Mybatis的一級緩存和二級緩存

預覽:

  • BaseExecutor:實作了Executor的全部方法,包括對緩存,事務,連接配接提供了一系列的模闆方法, 這些模闆方法中留出來了四個抽象的方法等待子類去實作如下
protected abstract int doUpdate(MappedStatement ms, Object parameter)
 throws SQLException;

protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
 throws SQLException;

protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
 throws SQLException;

protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
 throws SQLException;      
  • SimpleExecutor:特點是每次執行完畢後都會将建立出來的statement關閉掉,他也是預設的執行器類型
  • ReuseExecutor:在它在本地維護了一個容器,用來存放針對每條sql建立出來的statement,下次執行相同的sql時,會先檢查容器中是否存在相同的sql,如果存在就使用現成的,不再重複擷取
  • BatchExecutor:特點是進行批量修改,她會将修改操作記錄在本地,等待程式觸發送出事務,或者是觸發下一次查詢時,批量執行修改

建立執行器

當我們通過​

​SqlSessionFactory​

​​建立一個SqlSession時,執行​

​openSessionFromDataBase()​

​​方法時,會通過​

​newExecutor()​

​建立執行器:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }      

通過這個函數,可以找到上面列舉出來的所有的 執行器, MyBatis預設建立的執行器的類型的是SimpleExecutor,而且MyBatis預設開啟着對mapper的緩存(這其實就是Mybatis的二級緩存,但是,不論是注解版,還是xml版,都需要添加額外的配置才能使添加這個額外配置的mapper享受二級緩存,二級緩存被這個CachingExecutor維護着)

BaseExecutor 的模闆方法

在BaseExecutor的模本方法之前,其實省略了很多步驟,我們上一篇博文中有詳細的叙述,感興趣可以去看看,下面我就簡述一下: 程式員使用擷取到了mapper的代理對象,調用對象的​

​findAll()​

​​, 另外擷取到的sqlSession的實作也是預設的實作​

​DefaultSqlSession​

​​,這個sqlSession通過Executor嘗試去執行方法,哪個Executor呢? 就是我們目前要說的​

​CachingExecutor​

​​,調用它的​

​query()​

​​,這個方法是個模闆方法,因為​

​CachingExecutor​

​隻知道在什麼時間改做什麼,但是具體怎麼做,誰取做取決于它的實作類

如下是​

​BaseExecutor​

​​的​

​query()​

​方法

@Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }      

BaseExecutor維護的一級緩存

從上面的代碼中,其實我們就跟傳說中的Mybatis的一級緩存無限接近了,上面代碼中的邏輯很清楚,就是先檢查是否存在一級緩存,如果存在的話,就不再去建立statement查詢資料庫了

那問題來了,什麼是這個一級緩存呢? **一級緩存就是上面代碼中的​

​localCache​

​,如下圖: **

深入了解 Mybatis - Executor

再詳細一點就看下面這張圖:

深入了解 Mybatis - Executor

嗯! 原來傳說中的一級緩存叫localCache,它的封裝類叫​

​PerpetualCache​

​ 裡面維護了一個String 類型的id, 和一個hashMap 取名字也很講究,perpetual意味永不間斷,事實上确實如此,一級緩存預設存在,也關不了(至少我真的不知道),但是在與Spring整合時,Spring把這個緩存給關了,這并不奇怪,因為spring 直接幹掉了這個sqlSession

一級緩存什麼時候被填充的值呢?填充值的操作在一個叫做​

​queryFromDataBase()​

​的方法裡面,我截圖如下:

深入了解 Mybatis - Executor

其中的key=​

​1814536652:3224182340:com.changwu.dao.IUserDao.findAll:0:2147483647:select * from user:mysql​

其實看到這裡,平時聽到的為什麼大家會說一級緩存是屬于SqlSession的啊,諸如此類的話就是從這個看源碼的過程中的出來的結果,如果你覺的印象不深刻,我就接着補刀,每次和資料庫打交道都的先建立sqlSession,建立sqlSession的方法會在建立出DefaultSqlSession之前,先為它建立一個Executor,而我們說的一級緩存就是這個Executor的屬性

何時清空一級緩存

清空一級緩存的方法就是​

​BaseExecutor​

​​的​

​update()​

​方法

@Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 清空本地緩存
    clearLocalCache();
    // 調用子類執行器邏輯
    return doUpdate(ms, parameter);
  }      

SimpleExecutor

​SimpleExecutor​

​​是MyBatis提供的預設的執行器,他裡面封裝了MyBatis對JDBC的操作,但是雖然他叫​

​XXXExecutor​

​​,但是真正去CRUD的還真不是​

​SimpleExecutor​

​​,先看一下它是如何重寫​

​BaseExecutor​

​​的​

​doQuery()​

​方法的

詳細的過程在這篇博文中我就不往外貼代碼了,因為我在上一篇博文中有這塊源碼的詳細追蹤

@Override
 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);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }      

建立StatementHandler

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }      

雖然表面上看上面的代碼,感覺它隻會建立一個叫​

​RoutingStatementHandler​

​的handler,但是其實上這裡面有個秘密,根據MappedStatement 的不同,實際上他會建立三種不同類型的處理器,如下:

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        // 早期的普通查詢,極其容易被sql注入,不安全
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
       //  處理預編譯類型的sql語句
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
       // 處理存儲過程語句
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }      

建立PreParedStatement

執行查詢

關閉連接配接

關于​

​SimpleExecutor​

​​如何關閉statement,在上面一開始介紹​

​SimpleExecutor​

​​時,我其實就貼出來了,下面再這個叫做​

​closeStatement()​

​的函數詳情貼出來

protected void closeStatement(Statement statement) {
    if (statement != null) {
      try {
        statement.close();
      } catch (SQLException e) {
        // ignore
      }
    }
  }      

ReuseExecutor

這個ReuseExecutor相對于SimpleExecutor來說,不同點就是它先來的對Statement的複用,換句話說,某條Sql對應的Statement建立出來後被放在容器中儲存起來,再有使用這個statement的地方就是容器中拿就行了

他是怎麼實作的呢? 看看下面的代碼就知道了

public class ReuseExecutor extends BaseExecutor {
    private final Map<String, Statement> statementMap = new HashMap();

    public ReuseExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }      

嗯! 所謂的容器,不過是一個叫statementMap的HashMap而已

下一個問題: 這個容器什麼時候派上用場呢? 看看下面的代碼也就知道了--​

​this.hasStatementFor(sql)​

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        BoundSql boundSql = handler.getBoundSql();
        String sql = boundSql.getSql();
        Statement stmt;
        if (this.hasStatementFor(sql)) {
            stmt = this.getStatement(sql);
            this.applyTransactionTimeout(stmt);
        } else {
            Connection connection = this.getConnection(statementLog);
            stmt = handler.prepare(connection, this.transaction.getTimeout());
            this.putStatement(sql, stmt);
        }

        handler.parameterize(stmt);
        return stmt;
    }      

最後一點: 當MyBatis知道發生了事務的送出,復原等操作時,​

​ReuseExecutor​

​會批量關閉容器中的Statement

BatchExecutor

這個執行器相對于SimpleExecutor的特點是,它的​

​update()​

​方法是批量執行的

執行器送出或復原事務時會調用 doFlushStatements,進而批量執行送出的 sql 語句并最終批量關閉 statement 對象。

CachingExecutor與二級緩存

首先來說,這個​

​CachingExecutor​

​是什麼? 那就得看一下的屬性,如下:

public class CachingExecutor implements Executor {
  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();      

讓我們回想一下他的建立時機,沒錯就是在每次建立一個新的SqlSession時建立出來的,源碼如下,這就出現了一個驚天的大問号!!!,一級緩存和二級緩存為啥就一個屬于SqlSession級别,另一個卻被所有的SqlSession共享了? 這不是開玩笑呢? 我當時确實也是真的蒙,為啥他倆都是随時用随時new,包括上面代碼中的​

​TransactionalCacheManager​

​也是随時用随時new,憑什麼它維護的二級緩存就這麼牛? SqlSession挂掉後一級緩存也跟着挂掉,憑什麼二級緩存還在呢?

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }      

先說一下,我是看到哪行代碼後意識到二級緩存是這麼特殊的,如下:大家也看到了,下面代碼中的​

​tcm.getObject(cache, key);​

​,是我們上面新建立出來的​

​TransactionalCacheManager​

​,然後通過這個空白的對象的​

​getObject()​

​竟然就将緩存中的對象給擷取出來了,(我當時忽略了入參位置的cache,當然現在看,滿眼都是這個cache)

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);
  }      

我當時出現這個問題完全是我忽略了一部分前面解析配置檔案部分的源碼,下面我帶大家看看這部分源碼是怎麼執行的

一開始MyBatis會建立一個​

​XMLConfigBuilder​

​用這個builder去解析配置檔案(因為我們環境是單一的MyBatis,并沒有和其他架構整,這個builder就是用來解析配置檔案的)

我們關注什麼呢? 我們關注的是這個builder解析​

​<mapper>​

​标簽的,源碼入下:

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      ...
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));      

關注這個方法中的​

​configuration.addMapper(mapperInterface);​

​方法,如下: 這裡面存在一個對象叫做,MapperRegistry,這個對象叫做mapper的注冊器,其實我覺得這是個需要記住的對象,因為它出現的頻率還是挺多的,它幹什麼工作呢? 顧名思義,解析mapper呗 我的目前是基于注解搭建的環境,于是它這個MapperRegistry為我的mapper生成的對象就叫​

​MapperAnnotationBuilder​

​見名知意,這是個基于注解的建構器

public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }      

是以說我們就得去看看這個解析注解版本mapper的builder,到底是如何解析我提供的mapper的,源碼如下:

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }      

方法千千萬,但是我關注的是它的​

​ parseCache();​

​方法,為什麼我知道來這裡呢? (我靠!,我找了老半天...)

接下來就進入了一個高潮,相信你看到下面的代碼也會激動, 為什麼激動呢? 因為我們發現了Mybatis處理​

​@CacheNamespace​

​注解的細節資訊

private void parseCache() {
    CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
    if (cacheDomain != null) {
      Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
      Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
      Properties props = convertToProperties(cacheDomain.properties());
      assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
    }
  }      

再往下跟進這個​

​ assistant.useNewCache()​

​方法,就會發現,MyBatis将建立出來的一個Cache對象,這個Cache的實作類叫​

​BlockingCache​

建立出來的對象給誰了?

  • Configuration對象自己留了一份 (放在了 caches = new StrictMap<>("Caches collection");中)
  • 目前類​

    ​MapperBuilderAssistant​

    ​也保留一了一份
  • 最主要的是​

    ​MappedStatement​

    ​對象中也保留了一份​

    ​mappedStatement.cache ​

說了這麼多了,附上一張圖,用來紀念建立這個Cache的成員

深入了解 Mybatis - Executor

小結

其實上面建立這個Cache對象才是二級緩存者, 前面說的那個​

​CachingExecutor​

​​中的​

​TransactionalCacheManager​

​不過是擁有從這個Cache中擷取資料的能力而已

我有調試他是如何從Cache中擷取出緩存,事實證明,二級緩存中存放的不是對象,而是被序列化後存儲的資料,需要反序列化出來

下圖是Mybatis反序列化資料到新建立的對象中的截圖

深入了解 Mybatis - Executor

下圖是​

​TransactionalCacheManager​

​是如何從Cache中擷取資料的調用棧的截圖

深入了解 Mybatis - Executor

二級緩存與一級緩存的互斥性

第一點: 通過以上代碼的調用順序也能看出,二級緩存在一級緩存之前優先被執行, 也就是說二級緩存不存在,則查詢一級緩存,一級緩存再不存在,就查詢DB

第二點: 就是說,對于二級緩存來說,無論我們有沒有開啟事務的自動送出功能,都必須手動​

​commit()​

​二級緩存才能生效,否則二級緩存是沒有任何效果的

第三點: ​

​CachingExecutor ​

​送出事務時的源碼如下:

@Override
  public void commit(boolean required) throws SQLException {
    // 代理執行器送出
    delegate.commit(required);
    // 事務緩存管理器送出
    tcm.commit();
  }      

這就意味着,TransactionalCacheManager和BaseExecutor的實作類的事務都會被送出

為什麼說二級緩存和以及緩存互斥呢?可以看看BaseExecutor的源碼中​

​commit()​

​如下: 怎麼樣? 夠互斥吧,一個不​

​commit()​

​就不生效,​

​commit()​

​完事把一級緩存幹掉了

@Override
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache();
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }