Mybatis源碼分析-一級緩存【BaseExecutor】

本文主題:

  • Executor執(zhí)行體系回顧
  • 為什么要有一級緩存?
  • 一級緩存、二級緩存有什么區(qū)別?
  • 一級緩存屬于通用邏輯,那么結(jié)構(gòu)上它是如何設(shè)計的?
  • 一級緩存是用什么實現(xiàn)的?
  • 一級緩存命中條件有哪些?
  • 一級緩存有哪些清空場景?
  • Spring和Mybatis整合一級緩存失效?
  • 一級緩存的注意事項

Executor執(zhí)行體系

Executor執(zhí)行體系.png

這是從SqlSession到Executor實現(xiàn)的執(zhí)行體系圖,從圖中可以看出,一級緩存是在抽象類BaseExecutor實現(xiàn)的

為什么要有一級緩存?

框架自身方面

一級緩存能夠幫助Mybatis解決【結(jié)果集映射】的循環(huán)引用。假設(shè)張三有一張銀行卡,那么就會形成一種對象結(jié)構(gòu),用戶持有銀行卡,銀行卡歸屬于張三,這就是一種循環(huán)引用(或者理解為互相引用),mybatis在查詢用戶的時候查詢到了張三,發(fā)現(xiàn)張三持有銀行卡,則觸發(fā)了對張三銀行卡的查詢;查到了張三的銀行卡數(shù)據(jù)后,發(fā)現(xiàn)銀行卡有一個歸屬者信息,那又需要查詢用戶表,查到了張三,再查銀行卡,再查張三。。。。。
Mybatis為了解決這種循環(huán)應(yīng)用的結(jié)果映射,借助了一級緩存。具體的細節(jié),會在后面的結(jié)果集解析中再去分析,這里不再贅述

業(yè)務(wù)代碼方面

在程序員生涯中,大家一定遇到過類似的代碼。

  public void test(){
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    Long[] ids = {1,1,10,5,10};
    for(Long id : ids){
      //查詢用戶   如果沒有mybatis一級緩存,那么 id=1、id =10分別都會查詢兩次數(shù)據(jù)庫
      User user = userMapper.selectById(id);    
      //執(zhí)行針對該user的某業(yè)務(wù)邏輯
      this.doSomeThing(user);
    }
  }

上述代碼中,id=1、id=10其實沒有必要查詢兩次數(shù)據(jù)庫,這時候如果自己來解決,那可能需要這樣來做

public void test(){
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        Integer[] ids = {1,1,10,5,10};
        Map<Integer,User> userMap = new HashMap<Integer,User>();
        for(Integer id : ids){
            User user;
            if(userMap.containsKey(id)) {
                user = userMap.get(id);
            }else{
                //查詢用戶
                user = userMapper.selectById(id);
                userMap.put(id,user);
            }

            //執(zhí)行針對該user的某業(yè)務(wù)邏輯
            this.doSomeThing(user);
        }
    }

Mybatis的一級緩存,就可以解決這個場景的問題,即便第一種場景的代碼,也不用擔心id=1和id=10查詢了兩次的數(shù)據(jù)庫

一級緩存、二級緩存有什么區(qū)別?

一級緩存:會話級緩存,生命周期是整個會話SqlSession,非常短暫,不能直接關(guān)閉,不能跨線程使用
二級緩存:應(yīng)用級緩存,生命周期是整個應(yīng)用,可以跨線程使用

一級緩存屬于通用邏輯,那么結(jié)構(gòu)上它是如何設(shè)計的?

在Mybatis中,sql是由Executor來執(zhí)行的,Executor有3個實現(xiàn)類,如果在每一個實現(xiàn)類中都寫一遍這樣的代碼那就太過冗余了,因此抽象出了一個BaseExecutor類,用來處理一級緩存的相關(guān)邏輯


image.png

我們都是調(diào)用query、update兩個方法,query會調(diào)用子類實現(xiàn)doQuery,update會調(diào)用子類實現(xiàn)doUpdate

一級緩存是用什么實現(xiàn)的?

image.png

image.png

image.png

可以看到一級緩存是使用PerpetualCache實現(xiàn)的,而PerpetualCache內(nèi)部的真正實現(xiàn),就是一個HashMap。而PerpetualCache實現(xiàn)Cache接口,Cache接口的定義也非常簡單,核心接口就是 放入、獲取、 移除和清空緩存

一級緩存命中條件有哪些?

image.png

image.png

從上圖的createCacheKey方法,我們可以看到一級緩存CacheKey的組成,因此可以推斷出一級緩存的命中條件:
1、同一個SqlSession會話。不是同一個會話,就不是同一個localCache,這點很重要!??!
2、StatementId相同 。com.test.UserMapper.selectById
3、分頁參數(shù)RowBounds相同。limit 1
4、SQL語句相同。select id from user where id = ?
5、SQL查詢參數(shù)相同。 id =1
6、環(huán)境相同。environmentId=development 通常不會跨環(huán)境開發(fā),可以忽略

一級緩存有哪些清空場景?

image.png

BaseExecutor中有一個clearLocalCache方法用于清理一級緩存,那么找到調(diào)用這個方法的地方,也就找到了清空一級緩存的場景。如上圖所示,可以發(fā)現(xiàn)修改、查詢、提交、回滾,都可能會清空一級緩存,下面具體來看一下每一個場景是如何清空的

查詢:

 @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.");
    }

    //mapper.xml的sql塊上配置了flushCache=true,前置清空
    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 {
        //從數(shù)據(jù)庫中查詢,查詢到結(jié)果放入localCache
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      //mapper.xml的sql塊上配置了一級緩存作用域statementType="STATEMENT",后置清空
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
      }
    }
    return list;
  }

增刪改操作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.");
    }
    //執(zhí)行sql前清空一級緩存
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

提交:

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

回滾:

  @Override
  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        //回滾前清空
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }

清空一級緩存場景總結(jié):
1、執(zhí)行查詢時的清空,若配置了flushCache="true" ->前置清空; 若配置了statementType="STATEMENT" ->后置清空
2、執(zhí)行增刪改(update)操作
3、提交會話
4、回滾會話

Spring和Mybatis整合一級緩存失效?

Spring和Mybatis整合后,很多人發(fā)現(xiàn)一級緩存不能命中,這是因為Spring是通過SqlSessionTemplate保證了每次sql調(diào)用都會重新生成一個SqlSession會話,而一級緩存是會話級緩存,會話都不同了,自然不能命中。那么如何才能命中一級緩存呢?加一個事務(wù)注解@Transactional就可以了,因為在同一個事務(wù)方法中,如果Spring還去使用不同的SqlSession會話,那么就無法保證事務(wù)原子性。

一級緩存的注意事項

一級緩存的清空是清理掉會話中的全部緩存,它底層是調(diào)用了PerpetualCache的clear方法,也就是HashMap.clear。因此不存在指定sql清空的場景,比如會話中執(zhí)行了查詢用戶、查詢角色兩個sql,我們無法只清空一級緩存中的用戶數(shù)據(jù),只能全部清空。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。