mybatis二級(jí)緩存原理

mybatis篇

一級(jí)緩存的作用域是Sqlsession級(jí)別的顽爹,也就是說不同的Sqlsession是不會(huì)走一級(jí)緩存的啃匿,那么如果需要跨Sqlsession的緩存颠印,就需要使用到二級(jí)緩存了。

二級(jí)緩存的話默認(rèn)是關(guān)閉的睛廊,所以需要我們開啟已艰,開啟的方式官網(wǎng)也有介紹,需要在mybatis-config.xml核心配置文件中開啟二級(jí)緩存功能祟滴,并且我們mapper.xml中也需要加入<cache/>標(biāo)簽刊懈,二者缺一不可手幢,后面我們看源碼就能知道為啥這兩個(gè)缺一不可捷凄。

   <settings>
        <!-- 二級(jí)緩存 -->
        <setting name="cacheEnabled" value="true"/>
    </settings>
   <cache/>

先來看個(gè)例子

      public void cache() {
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        MybatisUserinfoMapper userinfoMapper = sqlSession.getMapper(MybatisUserinfoMapper.class);
        MybatisUserinfoModel model = userinfoMapper.selectByKey(4);
        System.out.println(model);
        System.out.println("=====================");
        // 執(zhí)行了查詢語句
        SqlSession session = sqlSessionFactory.openSession();
        MybatisUserinfoMapper mapper = session.getMapper(MybatisUserinfoMapper.class);
        MybatisUserinfoModel userinfoModel = mapper.selectByKey(4);
        System.out.println(userinfoModel);

        session.close();
        sqlSession.close();


    }

image.png

執(zhí)行結(jié)果很意外,為什么二級(jí)緩存的功能都開啟了围来,結(jié)果sql還是執(zhí)行了2次跺涤,并沒有走緩存,其實(shí)监透,二級(jí)緩存還有一個(gè)要注意的點(diǎn)那就是必須要提交事務(wù)二級(jí)緩存才會(huì)保存記錄桶错,因?yàn)橐呀?jīng)是跨SqlSession共享緩存了,所以事務(wù)必須要提交胀蛮,否則會(huì)讀取到因混滾導(dǎo)致的錯(cuò)誤數(shù)據(jù)院刁。

    public void cache() {
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        MybatisUserinfoMapper userinfoMapper = sqlSession.getMapper(MybatisUserinfoMapper.class);
        MybatisUserinfoModel model = userinfoMapper.selectByKey(4);
        System.out.println(model);
        System.out.println("=====================");
        // 需要提交事務(wù)
        sqlSession.commit();
        // 執(zhí)行了查詢語句
        SqlSession session = sqlSessionFactory.openSession();
        MybatisUserinfoMapper mapper = session.getMapper(MybatisUserinfoMapper.class);
        MybatisUserinfoModel userinfoModel = mapper.selectByKey(4);
        System.out.println(userinfoModel);

        session.close();
        sqlSession.close();


    }

image.png

可以看到,第二次查詢沒有走sql,直接從二級(jí)緩存中獲取的值粪狼。
這里我們就不再具體分析sqlsession的獲取和mapper的獲取了退腥,具體可以看之前的文章mybatis一級(jí)緩存原理

有個(gè)地方需要注意,二級(jí)緩存的Sqlsession中的Executor實(shí)際上是CachingExecutor

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  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);
    }
    // 這就是必須要配置cacheEnabled的值再榄,否則不會(huì)存在二級(jí)緩存
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

我們知道getMapper最終的執(zhí)行都會(huì)走到MapperProxy類中的invoker方法狡刘,具體就來分析這個(gè)類。

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

  @Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.<T>selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

最后來到了重點(diǎn)的地方

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

CacheKey我們可以認(rèn)為他就是每個(gè)方法對應(yīng)的一個(gè)唯一標(biāo)識(shí)符困鸥。

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // mapper.xml中我們配置的<cache/>標(biāo)簽
    Cache cache = ms.getCache();
    if (cache != null) {
      // 如果flushCache配置為了true颓帝,那么就會(huì)清空一級(jí)緩存和二級(jí)緩存
      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.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

這里我們就可以看出為什么之前兩者必須要配置,cacheEnable開啟了才會(huì)用CachingExecutor包裝一下BaseExecutor窝革,而<cache/>標(biāo)簽只有配置了才會(huì)走緩存的邏輯

List<E> list = (List<E>) tcm.getObject(cache, key);

這里的tcm

private final TransactionalCacheManager tcm = new TransactionalCacheManager();
  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }
public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  private final Cache delegate;
  private boolean clearOnCommit;
  private final Map<Object, Object> entriesToAddOnCommit;
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<Object, Object>();
    this.entriesMissedInCache = new HashSet<Object>();
  }

  @Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

到這,我們就差不多揭開了二級(jí)緩存的秘密吕座,重要的還是<cache/>這個(gè)標(biāo)簽虐译,因?yàn)樗拇嬖诰蛯?yīng)著每個(gè)mapper.xml中的一個(gè)具體Cache類,而這個(gè)類在每個(gè)mapper.xml中又是同一個(gè)吴趴,所以最終的值是放入了Cache類中漆诽,key為CacheKey,value就是sql執(zhí)行的結(jié)果锣枝。
至于為什么需要事務(wù)提交才能命中二級(jí)緩存厢拭,我們看下put方法就知道

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

這里的putObject并沒有真正的把值存入Cache中,而是存入了待提交的Map中撇叁,所以再來看下commit做了什么

  @Override
  public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  @Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

具體看tcm.commit()


  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

而這里可以看到此處會(huì)遍歷所有的TransactionCache并執(zhí)行commit方法


  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

真相就出來了供鸠,會(huì)遍歷待提交的Map然后把里面的值都存入Cache中,所以后面的查詢就能直接從Cache中拿到值了陨闹。

總結(jié)
二級(jí)緩存先會(huì)把Sqlsession中的Executor包裝成包裝成CacheingExecutor,所有的sql都會(huì)經(jīng)過這個(gè)類楞捂,而該類通過mapper.xml中配置的唯一<cache/>標(biāo)簽生成的Cache類存放每個(gè)方法執(zhí)行的結(jié)果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末薄坏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子寨闹,更是在濱河造成了極大的恐慌胶坠,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件繁堡,死亡現(xiàn)場離奇詭異沈善,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)椭蹄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門闻牡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人塑娇,你說我怎么就攤上這事澈侠。” “怎么了埋酬?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵哨啃,是天一觀的道長。 經(jīng)常有香客問我写妥,道長拳球,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任珍特,我火速辦了婚禮祝峻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扎筒。我一直安慰自己莱找,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布嗜桌。 她就那樣靜靜地躺著奥溺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪骨宠。 梳的紋絲不亂的頭發(fā)上浮定,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音层亿,去河邊找鬼桦卒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛匿又,可吹牛的內(nèi)容都是我干的方灾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼琳省,長吁一口氣:“原來是場噩夢啊……” “哼迎吵!你這毒婦竟也來了躲撰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤击费,失蹤者是張志新(化名)和其女友劉穎拢蛋,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔫巩,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡谆棱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了圆仔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垃瞧。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖坪郭,靈堂內(nèi)的尸體忽然破棺而出个从,到底是詐尸還是另有隱情,我是刑警寧澤歪沃,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布嗦锐,位于F島的核電站,受9級(jí)特大地震影響沪曙,放射性物質(zhì)發(fā)生泄漏奕污。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一液走、第九天 我趴在偏房一處隱蔽的房頂上張望碳默。 院中可真熱鬧,春花似錦缘眶、人聲如沸嘱根。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽儿子。三九已至,卻和暖如春砸喻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蒋譬。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國打工割岛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人犯助。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓癣漆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親剂买。 傳聞我的和親對象是個(gè)殘疾皇子惠爽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354