深刻理解 Mybatis - Executor

承接上篇博客, 本文探究MyBatis中的Executor, 以下圖: 是Executor體系圖html

executor體系圖

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

預覽:mysql

  • 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維護着)spring

BaseExecutor 的模板方法

在BaseExecutor的模本方法以前,其實省略了不少步驟,咱們上一篇博文中有詳細的敘述,感興趣能夠去看看,下面我就簡述一下: 程序員使用獲取到了mapper的代理對象,調用對象的findAll(), 另外獲取到的sqlSession的實現也是默認的實現DefaultSqlSession,這個sqlSession經過Executor嘗試去執行方法,哪一個Executor呢? 就是咱們當前要說的CachingExecutor,調用它的query(),這個方法是個模板方法,由於CachingExecutor只知道在什麼時間改作什麼,可是具體怎麼作,誰取作取決於它的實現類sql

以下是BaseExecutorquery()方法數據庫

@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,以下圖: 安全

一級緩存

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

一級緩存

嗯! 原來傳說中的一級緩存叫localCache,它的封裝類叫PerpetualCache 裏面維護了一個String 類型的id, 和一個hashMap 取名字也很講究,perpetual意味永不間斷,事實上確實如此,一級緩存默認存在,也關不了(至少我真的不知道),可是在與Spring整合時,Spring把這個緩存給關了,這並不奇怪,由於spring 直接幹掉了這個sqlSession

一級緩存何時被填充的值呢?填充值的操做在一個叫作queryFromDataBase()的方法裏面,我截圖以下:

填充一級緩存

其中的key=1814536652:3224182340:com.changwu.dao.IUserDao.findAll:0:2147483647:select * from user:mysql

其實看到這裏,平時聽到的爲何你們會說一級緩存是屬於SqlSession的啊,諸如此類的話就是從這個看源碼的過程當中的出來的結果,若是你覺的印象不深入,我就接着補刀,每次和數據庫打交道都的先建立sqlSession,建立sqlSession的方法會在建立出DefaultSqlSession以前,先爲它建立一個Executor,而咱們說的一級緩存就是這個Executor的屬性

什麼時候清空一級緩存

清空一級緩存的方法就是BaseExecutorupdate()方法

@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,先看一下它是如何重寫BaseExecutordoQuery()方法的

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

@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

點擊進入上篇博文,查看如何建立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的成員

建立二級緩存Cache體系

小結

其實上面建立這個Cache對象纔是二級緩存者, 前面說的那個CachingExecutor中的TransactionalCacheManager不過是擁有從這個Cache中獲取數據的能力而已

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

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

反序列化

下圖是TransactionalCacheManager是如何從Cache中獲取數據的調用棧的截圖

從caching中獲取數據調用棧

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

第一點: 經過以上代碼的調用順序也能看出,二級緩存在一級緩存以前優先被執行, 也就是說二級緩存不存在,則查詢一級緩存,一級緩存再不存在,就查詢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();
    }
  }

到這裏本文又行將結束了,整體的節奏仍是挺歡快挺帶勁的,我是bloger-賜我白日夢,若是有錯誤歡迎指出,也歡迎您點贊支持...

相關文章
相關標籤/搜索