MyBatis源碼解析(三)—緩存篇

前言

大家好,這一篇文章是MyBatis系列的最后一篇文章戳气,前面兩篇文章《MyBatis源碼解析(一)—構(gòu)建篇》《MyBatis源碼解析(二)—執(zhí)行篇》,主要說明了MyBatis是如何將我們的xml配置文件構(gòu)建為其內(nèi)部的Configuration對象和MappedStatement對象的巧鸭,然后在第二篇我們說了構(gòu)建完成后MyBatis是如何一步一步地執(zhí)行我們的SQL語句并且對結(jié)果集進行封裝的瓶您。那么這篇作為MyBatis系列的最后一篇,自然是要來聊聊MyBatis中的一個不可忽視的功能纲仍,一級緩存和二級緩存览闰。

何謂緩存?

雖然這篇說的是MyBatis的緩存巷折,但是我希望正在學習計算機的小伙伴即使還沒有使用過MyBatis框架也能看明白今天這篇文章压鉴。

緩存是什么?我來說說個人的理解锻拘,最后再上比較官方的概念油吭。

緩存(Cache),顧名思義署拟,有臨時存儲的意思婉宰。計算機中的緩存,我們可以直接理解為推穷,存儲在內(nèi)存中的數(shù)據(jù)的容器心包,這與物理存儲是有差別的,由于內(nèi)存的讀寫速度比物理存儲高出幾個數(shù)量級馒铃,所以程序直接從內(nèi)存中取數(shù)據(jù)和從物理硬盤中取數(shù)據(jù)的效率是不同的蟹腾,所以有一些經(jīng)常需要讀取的數(shù)據(jù),設(shè)計師們通常會將其放在緩存中区宇,以便于程序?qū)ζ溥M行讀取娃殖。但是,緩存是有代價的议谷,剛才我們說過炉爆,緩存就是在內(nèi)存中的數(shù)據(jù)的容器,一條64G的內(nèi)存條卧晓,通撤沂祝可以買3-4塊1T-2T的機械硬盤了,所以緩存不能無節(jié)制地使用逼裆,這樣成本會劇增郁稍,所以一般緩存中的數(shù)據(jù)都是需要頻繁查詢,但是又不常修改的數(shù)據(jù)波附。

而在一般業(yè)務(wù)中艺晴,查詢通常會經(jīng)過如下步驟昼钻。

讀操作 --> 查詢緩存中已經(jīng)存在數(shù)據(jù) -->如果不存在則查詢數(shù)據(jù)庫掸屡,如果存在則直接查詢緩存-->數(shù)據(jù)庫查詢返回數(shù)據(jù)的同時封寞,寫入緩存中。

寫操作 --> 清空緩存數(shù)據(jù) -->寫入數(shù)據(jù)庫

緩存流程

比較官方的概念

  ? 緩存就是數(shù)據(jù)交換的緩沖區(qū)(稱作:Cache)仅财,當某一硬件要讀取數(shù)據(jù)時狈究,會首先從緩存匯總查詢數(shù)據(jù),有則直接執(zhí)行盏求,不存在時從內(nèi)存中獲取抖锥。由于緩存的數(shù)據(jù)比內(nèi)存快的多,所以緩存的作用就是幫助硬件更快的運行碎罚。
  ? 緩存往往使用的是RAM(斷電既掉的非永久存儲)磅废,所以在用完后還是會把文件送到硬盤等存儲器中永久存儲。電腦中最大緩存就是內(nèi)存條荆烈,硬盤上也有16M或者32M的緩存拯勉。
  ? 高速緩存是用來協(xié)調(diào)CPU與主存之間存取速度的差異而設(shè)置的。一般CPU工作速度高憔购,但內(nèi)存的工作速度相對較低宫峦,為了解決這個問題,通常使用高速緩存玫鸟,高速緩存的存取速度介于CPU與主存之間导绷。系統(tǒng)將一些CPU在最近幾個時間段經(jīng)常訪問的內(nèi)容存在高速緩存,這樣就在一定程度上緩解了由于主存速度低造成的CPU“停工待料”的情況屎飘。
  ? 緩存就是把一些外存上的數(shù)據(jù)保存在內(nèi)存上而已妥曲,為什么保存在內(nèi)存上,我們運行的所有程序里面的變量都是存放在內(nèi)存中的钦购,所以如果想將值放入內(nèi)存上逾一,可以通過變量的方式存儲。在JAVA中一些緩存一般都是通過Map集合來實現(xiàn)的肮雨。

MyBatis的緩存

在說MyBatis的緩存之前遵堵,先了解一下Java中的緩存一般都是怎么實現(xiàn)的,我們通常會使用Java中的Map怨规,來實現(xiàn)緩存陌宿,所以在之后的緩存這個概念,就可以把它直接理解為一個Map波丰,存的就是鍵值對壳坪。

  • 一級緩存簡介

    MyBatis中的一級緩存,是默認開啟且無法關(guān)閉的掰烟,一級緩存默認的作用域是一個SqlSession爽蝴,解釋一下沐批,就是當SqlSession被構(gòu)建了之后,緩存就存在了蝎亚,只要這個SqlSession不關(guān)閉九孩,這個緩存就會一直存在,換言之发框,只要SqlSession不關(guān)閉躺彬,那么這個SqlSession處理的同一條SQL就不會被調(diào)用兩次,只有當會話結(jié)束了之后梅惯,這個緩存才會一并被釋放宪拥。

    雖說我們不能關(guān)閉一級緩存,但是作用域是可以修改的铣减,比如可以修改為某個Mapper她君。

    一級緩存的生命周期:

    1、如果SqlSession調(diào)用了close()方法葫哗,會釋放掉一級緩存PerpetualCache對象缔刹,一級緩存將不可用。

    2魄梯、如果SqlSession調(diào)用了clearCache()桨螺,會清空PerpetualCache對象中的數(shù)據(jù),但是該對象仍可使用酿秸。

    3灭翔、SqlSession中執(zhí)行了任何一個update操作(update()、delete()辣苏、insert()) 肝箱,都會清空PerpetualCache對象的數(shù)據(jù),但是該對象可以繼續(xù)使用稀蟋。

    節(jié)選自:https://www.cnblogs.com/happyflyingpig/p/7739749.html

    MyBatis一級緩存簡單示意圖
  • 二級緩存簡介

    MyBatis的二級緩存是默認關(guān)閉的煌张,如果要開啟有兩種方式:

    1. 在mybatis-config.xml中加入如下配置片段

      <!-- 全局配置參數(shù),需要時再設(shè)置 -->
      <settings>
             <!-- 開啟二級緩存  默認值為true -->
          <setting name="cacheEnabled" value="true"/>
      
      </settings>
      
    2. 在mapper.xml中開啟

      <!--開啟本mapper的namespace下的二級緩存-->
      <!--
              eviction:代表的是緩存回收策略退客,目前MyBatis提供以下策略骏融。
              (1) LRU,最近最少使用的,一處最長時間不用的對象
              (2) FIFO,先進先出萌狂,按對象進入緩存的順序來移除他們
              (3) SOFT,軟引用档玻,移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象
              (4) WEAK,弱引用,更積極的移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象茫藏。
                 這里采用的是LRU误趴,  移除最長時間不用的對形象
      
              flushInterval:刷新間隔時間,單位為毫秒务傲,如果你不配置它凉当,那么當
              SQL被執(zhí)行的時候才會去刷新緩存枣申。
      
              size:引用數(shù)目,一個正整數(shù)看杭,代表緩存最多可以存儲多少個對象忠藤,不宜設(shè)置過大。設(shè)置過大會導致內(nèi)存溢出泊窘。
              這里配置的是1024個對象
      
              readOnly:只讀熄驼,意味著緩存數(shù)據(jù)只能讀取而不能修改像寒,這樣設(shè)置的好處是我們可以快速讀取緩存烘豹,缺點是我們沒有
              辦法修改緩存,他的默認值是false诺祸,不允許我們修改
       -->
      <cache eviction="回收策略" type="緩存類"/>
      

    二級緩存的作用域與一級緩存不同携悯,一級緩存的作用域是一個SqlSession,但是二級緩存的作用域是一個namespace筷笨,什么意思呢憔鬼,你可以把它理解為一個mapper,在這個mapper中操作的所有SqlSession都可以共享這個二級緩存胃夏。但是假設(shè)有兩條相同的SQL轴或,寫在不同的namespace下,那這個SQL就會被執(zhí)行兩次仰禀,并且產(chǎn)生兩份value相同的緩存照雁。

MyBatis緩存的執(zhí)行流程

依舊是用前兩篇的測試用例,我們從源碼的角度看看緩存是如何執(zhí)行的答恶。

public static void main(String[] args) throws Exception {
    String resource = "mybatis.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //從調(diào)用者角度來講 與數(shù)據(jù)庫打交道的對象 SqlSession
    DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
    Map<String,Object> map = new HashMap<>();
    map.put("id","2121");
    //執(zhí)行這個方法實際上會走到invoke
    System.out.println(mapper.selectAll(map));
    sqlSession.close();
    sqlSession.commit();
  }

這里會執(zhí)行到query()方法:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //二級緩存的Cache,通過MappedStatement獲取
    Cache cache = ms.getCache();
    if (cache != null) {
      //是否需要刷新緩存
      //在<select>標簽中也可以配置flushCache屬性來設(shè)置是否查詢前要刷新緩存饺蚊,默認增刪改刷新緩存查詢不刷新
      flushCacheIfRequired(ms);
      //判斷這個mapper是否開啟了二級緩存
      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);
          //查詢完畢后將數(shù)據(jù)放入二級緩存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        //返回
        return list;
      }
    }
    //如果二級緩存為null悬嗓,那么直接查詢一級緩存
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

可以看到首先MyBatis在查詢數(shù)據(jù)時會先看看這個mapper是否開啟了二級緩存污呼,如果開啟了,會先查詢二級緩存包竹,如果緩存中存在我們需要的數(shù)據(jù)燕酷,那么直接就從緩存返回數(shù)據(jù),如果不存在周瞎,則繼續(xù)往下走查詢邏輯苗缩。

接著往下走,如果二級緩存不存在堰氓,那么就直接查詢數(shù)據(jù)了嗎挤渐?答案是否定的,二級緩存如果不存在双絮,MyBatis會再查詢一次一級緩存浴麻,接著往下看得问。

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++;
      //查詢一級緩存(localCache)
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
          //對于存儲過程有輸出資源的處理
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
          //如果緩存為空,則從數(shù)據(jù)庫拿
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
         /**這個是queryFromDatabase的邏輯
         * //先往緩存中put一個占位符
            localCache.putObject(key, EXECUTION_PLACEHOLDER);
            try {
              list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
            } finally {
              localCache.removeObject(key);
            }
            //往一級緩存中put真實數(shù)據(jù)
            localCache.putObject(key, list);
            if (ms.getStatementType() == StatementType.CALLABLE) {
              localOutputParameterCache.putObject(key, parameter);
            }
            return list;
         */
      }
    } 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;
  }

一級緩存和二級緩存的查詢邏輯其實差不多软免,都是先查詢緩存宫纬,如果沒有則進行下一步查詢,只不過一級緩存中如果沒有結(jié)果膏萧,那么就直接查詢數(shù)據(jù)庫漓骚,然后回寫一級緩存。

講到這里其實一級緩存和二級緩存的執(zhí)行流程就說完了榛泛,緩存的邏輯其實都差不多蝌蹂,MyBatis的緩存是先查詢一級緩存再查詢二級緩存

但是文章到這里并沒有結(jié)束曹锨,還有一些緩存相關(guān)的問題可以聊孤个。

緩存事務(wù)問題

不知道這個問題大家有沒有想過,假設(shè)有這么一個場景沛简,這里用二級緩存舉例齐鲤,因為二級緩存是跨事務(wù)的。

假設(shè)我們在查詢之前開啟了事務(wù)椒楣,并且進行數(shù)據(jù)庫操作:

1.往數(shù)據(jù)庫中插入一條數(shù)據(jù)(INSERT)

2.在同一個事務(wù)內(nèi)查詢數(shù)據(jù)(SELECT)

3.提交事務(wù)(COMMIT)

4.提交事務(wù)失敗(ROLLBACK)

我們來分析一下這個場景给郊,首先SqlSession先執(zhí)行了一個INSERT操作,很顯然捧灰,在我們剛才分析的邏輯基礎(chǔ)上淆九,此時緩存一定會被清空,然后在同一個事務(wù)下查詢數(shù)據(jù)凤壁,數(shù)據(jù)又從數(shù)據(jù)庫中被加載到了緩存中吩屹,此時提交事務(wù),然后事務(wù)提交失敗了拧抖∶核眩考慮一下此時會出現(xiàn)什么情況,相信已經(jīng)有人想到了唧席,事務(wù)提交失敗之后擦盾,事務(wù)會進行回滾,那么執(zhí)行INSERT插入的這條數(shù)據(jù)就被回滾了淌哟,但是我們在插入之后進行了一次查詢迹卢,這個數(shù)據(jù)已經(jīng)放到了緩存中,下一次查詢必然是直接查詢緩存而不會再去查詢數(shù)據(jù)庫了徒仓,可是此時緩存和數(shù)據(jù)庫之間已經(jīng)存在了數(shù)據(jù)不一致的問題腐碱。

問題的根本原因就在于,數(shù)據(jù)庫提交事務(wù)失敗了可以進行回滾,但是緩存不能進行回滾症见。

我們來看看MyBatis是如何解決這個問題的喂走。

  • TransactionalCacheManager

    這個類是MyBatis用于緩存事務(wù)管理的類,我們可以看看其數(shù)據(jù)結(jié)構(gòu)谋作。

    public class TransactionalCacheManager {
    
     //事務(wù)緩存
      private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
    
      public void clear(Cache cache) {
        getTransactionalCache(cache).clear();
      }
    
      public Object getObject(Cache cache, CacheKey key) {
        return getTransactionalCache(cache).getObject(key);
      }
    
      public void putObject(Cache cache, CacheKey key, Object value) {
        getTransactionalCache(cache).putObject(key, value);
      }
    
      public void commit() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
          txCache.commit();
        }
      }
    
      public void rollback() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
          txCache.rollback();
        }
      }
    
      private TransactionalCache getTransactionalCache(Cache cache) {
        return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
      }
    
    }
    

    TransactionalCacheManager中封裝了一個Map芋肠,用于將事務(wù)緩存對象緩存起來,這個Map的Key是我們的二級緩存對象遵蚜,而Value是一個叫做TransactionalCache帖池,顧名思義,這個緩存就是事務(wù)緩存吭净,我們來看看其內(nèi)部的實現(xiàn)睡汹。

    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;
      //未命中的緩存集合,防止擊穿緩存攒钳,并且如果查詢到的數(shù)據(jù)為null帮孔,說明要通過數(shù)據(jù)庫查詢雷滋,有可能存在數(shù)據(jù)不一致不撑,都記錄到這個地方
      private final Set<Object> entriesMissedInCache;
    
      public TransactionalCache(Cache delegate) {
        this.delegate = delegate;
        this.clearOnCommit = false;
        this.entriesToAddOnCommit = new HashMap<>();
        this.entriesMissedInCache = new HashSet<>();
      }
    
      @Override
      public String getId() {
        return delegate.getId();
      }
    
      @Override
      public int getSize() {
        return delegate.getSize();
      }
    
      @Override
      public Object getObject(Object key) {
        // issue #116
        Object object = delegate.getObject(key);
        if (object == null) {
          //如果取出的是空,那么放到未命中緩存晤斩,并且在查詢數(shù)據(jù)庫之后putObject中將本應該放到真實緩存中的鍵值對放到待提交事務(wù)緩存
          entriesMissedInCache.add(key);
        }
        //如果不為空
        // issue #146
        //查看緩存清空標識是否為false焕檬,如果事務(wù)提交了就為true,事務(wù)提交了會更新緩存澳泵,所以返回null实愚。
        if (clearOnCommit) {
          return null;
        } else {
          //如果事務(wù)沒有提交,那么返回原先緩存中的數(shù)據(jù)兔辅,
          return object;
        }
      }
    
      @Override
      public void putObject(Object key, Object object) {
          //如果返回的數(shù)據(jù)為null腊敲,那么有可能到數(shù)據(jù)庫查詢,查詢到的數(shù)據(jù)先放置到待提交事務(wù)的緩存中
        //本來應該put到緩存中维苔,現(xiàn)在put到待提交事務(wù)的緩存中去碰辅。
        entriesToAddOnCommit.put(key, object);
      }
    
      @Override
      public Object removeObject(Object key) {
        return null;
      }
    
      @Override
      public void clear() {
        //如果事務(wù)提交了,那么將清空緩存提交標識設(shè)置為true
        clearOnCommit = true;
        //清空entriesToAddOnCommit
        entriesToAddOnCommit.clear();
      }
    
      public void commit() {
        if (clearOnCommit) {
          //如果為true介时,那么就清空緩存没宾。
          delegate.clear();
        }
        //把本地緩存刷新到真實緩存。
        flushPendingEntries();
        //然后將所有值復位沸柔。
        reset();
      }
    
      public void rollback() {
        //事務(wù)回滾
        unlockMissedEntries();
        reset();
      }
    
      private void reset() {
        //復位操作循衰。
        clearOnCommit = false;
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
      }
    
      private void flushPendingEntries() {
        //遍歷事務(wù)管理器中待提交的緩存
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
          //寫入到真實的緩存中。
          delegate.putObject(entry.getKey(), entry.getValue());
        }
        for (Object entry : entriesMissedInCache) {
          //把未命中的一起put
          if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
          }
        }
      }
    
      private void unlockMissedEntries() {
        for (Object entry : entriesMissedInCache) {
          //清空真實緩存區(qū)中未命中的緩存褐澎。
        try {
            delegate.removeObject(entry);
          } catch (Exception e) {
            log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
                + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
          }
        }
      }
    
    }
    

    在TransactionalCache中有一個真實緩存對象Cache会钝,這個真實緩存對象就是我們真正的二級緩存,還有一個 entriesToAddOnCommit工三,這個Map對象中存放的是所有待提交事務(wù)的緩存迁酸。

    我們在二級緩存執(zhí)行的代碼中咽弦,看到在緩存中g(shù)et或者put結(jié)果時,都是叫tcm的對象調(diào)用了getObject()方法和putObject()方法胁出,這個對象實際上就是TransactionalCacheManager的實體對象型型,而這個對象實際上是調(diào)用了TransactionalCache的方法,我們來看看這兩個方法是如何實現(xiàn)的全蝶。

    @Override
    public Object getObject(Object key) {
        // issue #116
        Object object = delegate.getObject(key);
        if (object == null) {
          //如果取出的是空闹蒜,那么放到未命中緩存,并且在查詢數(shù)據(jù)庫之后putObject中將本應該放到真實緩存中的鍵值對放到待提交事務(wù)緩存
          entriesMissedInCache.add(key);
        }
        //如果不為空
        // issue #146
        //查看緩存清空標識是否為false抑淫,如果事務(wù)提交了就為true绷落,事務(wù)提交了會更新緩存,所以返回null始苇。
        if (clearOnCommit) {
          return null;
        } else {
          //如果事務(wù)沒有提交砌烁,那么返回原先緩存中的數(shù)據(jù),
          return object;
        }
    }
    @Override
    public void putObject(Object key, Object object) {
          //如果返回的數(shù)據(jù)為null催式,那么有可能到數(shù)據(jù)庫查詢函喉,查詢到的數(shù)據(jù)先放置到待提交事務(wù)的緩存中
        //本來應該put到緩存中,現(xiàn)在put到待提交事務(wù)的緩存中去荣月。
        entriesToAddOnCommit.put(key, object);
    }
    

    在getObject()方法中存在兩個分支:

    如果發(fā)現(xiàn)緩存中取出的數(shù)據(jù)為null管呵,那么會把這個key放到entriesMissedInCache中,這個對象的主要作用就是將我們未命中的key全都保存下來哺窄,防止緩存被擊穿捐下,并且當我們在緩存中無法查詢到數(shù)據(jù),那么就有可能到一級緩存和數(shù)據(jù)庫中查詢萌业,那么查詢過后會調(diào)用putObject()方法坷襟,這個方法本應該將我們查詢到的數(shù)據(jù)put到真是緩存中,但是現(xiàn)在由于存在事務(wù)生年,所以暫時先放到entriesToAddOnCommit中婴程。

    如果發(fā)現(xiàn)緩存中取出的數(shù)據(jù)不為null,那么會查看事務(wù)提交標識(clearOnCommit)是否為true晶框,如果為true排抬,代表事務(wù)已經(jīng)提交了,之后緩存會被清空授段,所以返回null蹲蒲,如果為false,那么由于事務(wù)還沒有被提交侵贵,所以返回當前緩存中存的數(shù)據(jù)届搁。

    那么當事務(wù)提交成功或提交失敗,又會是什么狀況呢?不妨看看commit和rollback方法卡睦。

    public void commit() {
        if (clearOnCommit) {
          //如果為true宴胧,那么就清空緩存。
          delegate.clear();
        }
        //把本地緩存刷新到真實緩存表锻。
        flushPendingEntries();
        //然后將所有值復位恕齐。
        reset();
    }
    
    public void rollback() {
        //事務(wù)回滾
        unlockMissedEntries();
        reset();
    }
    

    先分析事務(wù)提交成功的情況,如果事務(wù)正常提交了瞬逊,那么會有這么幾步操作:

    1. 清空真實緩存显歧。
    2. 將本地緩存(未提交的事務(wù)緩存 entriesToAddOnCommit)刷新到真實緩存。
    3. 將所有值復位确镊。

    我們來看看代碼是如何實現(xiàn)的:

    private void flushPendingEntries() {
        //遍歷事務(wù)管理器中待提交的緩存
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
          //寫入到真實的緩存中士骤。
          delegate.putObject(entry.getKey(), entry.getValue());
        }
        for (Object entry : entriesMissedInCache) {
          //把未命中的一起put
          if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
          }
        }
    }
    private void reset() {
        //復位操作。
        clearOnCommit = false;
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
    }
    public void clear() {
      //如果事務(wù)提交了蕾域,那么將清空緩存提交標識設(shè)置為true
        clearOnCommit = true;
        //清空事務(wù)提交緩存
        entriesToAddOnCommit.clear();
    }
    

    清空真實緩存就不說了拷肌,就是Map調(diào)用clear方法,清空所有的鍵值對旨巷。

    將未提交事務(wù)緩存刷新到真實緩存巨缘,首先會遍歷entriesToAddOnCommit,然后調(diào)用真實緩存的putObject方法契沫,將entriesToAddOnCommit中的鍵值對put到真實緩存中带猴,這步完成后,還會將未命中緩存中的數(shù)據(jù)一起put進去懈万,值設(shè)置為null。

    最后進行復位靶病,將提交事務(wù)標識設(shè)為false会通,未命中緩存、未提交事務(wù)緩存中的所有數(shù)據(jù)全都清空娄周。

    如果事務(wù)沒有正常提交涕侈,那么就會發(fā)生回滾,再來看看回滾是什么流程:

    1. 清空真實緩存中未命中的緩存煤辨。
    2. 將所有值復位
    public void rollback() {
        //事務(wù)回滾
        unlockMissedEntries();
        reset();
    }
    
    private void unlockMissedEntries() {
        for (Object entry : entriesMissedInCache) {
          //清空真實緩存區(qū)中未命中的緩存裳涛。
        try {
            delegate.removeObject(entry);
          } catch (Exception e) {
            log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
                + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
          }
        }
    }
    

    由于凡是在緩存中未命中的key,都會被記錄到entriesMissedInCache這個緩存中众辨,所以這個緩存中包含了所有查詢數(shù)據(jù)庫的key端三,所以最終只需要在真實緩存中把這部分key和對應的value給刪除即可。

  • 緩存事務(wù)總結(jié)

    簡而言之鹃彻,緩存事務(wù)的控制主要是通過TransactionalCacheManager控制TransactionCache完成的郊闯,關(guān)鍵就在于TransactionCache中的entriesToAddCommit和entriesMissedInCache這兩個對象,entriesToAddCommit在事務(wù)開啟到提交期間作為真實緩存的替代品,將從數(shù)據(jù)庫中查詢到的數(shù)據(jù)先放到這個Map中团赁,待事務(wù)提交后育拨,再將這個對象中的數(shù)據(jù)刷新到真實緩存中,如果事務(wù)提交失敗了欢摄,則清空這個緩存中的數(shù)據(jù)即可熬丧,并不會影響到真實的緩存。

    entriesMissedInCache主要是用來保存在查詢過程中在緩存中沒有命中的key怀挠,由于沒有命中锹引,說明需要到數(shù)據(jù)庫中查詢,那么查詢過后會保存到entriesToAddCommit中唆香,那么假設(shè)在事務(wù)提交過程中失敗了嫌变,而此時entriesToAddCommit的數(shù)據(jù)又都刷新到緩存中了,那么此時調(diào)用rollback就會通過entriesMissedInCache中保存的key躬它,來清理真實緩存腾啥,這樣就可以保證在事務(wù)中緩存數(shù)據(jù)與數(shù)據(jù)庫的數(shù)據(jù)保持一致。

    緩存事務(wù)

一些使用緩存的經(jīng)驗

  • 二級緩存不能存在一直增多的數(shù)據(jù)

    由于二級緩存的影響范圍不是SqlSession而是namespace冯吓,所以二級緩存會在你的應用啟動時一直存在直到應用關(guān)閉倘待,所以二級緩存中不能存在隨著時間數(shù)據(jù)量越來越大的數(shù)據(jù),這樣有可能會造成內(nèi)存空間被占滿组贺。

  • 二級緩存有可能存在臟讀的問題(可避免)

    由于二級緩存的作用域為namespace凸舵,那么就可以假設(shè)這么一個場景,有兩個namespace操作一張表失尖,第一個namespace查詢該表并回寫到內(nèi)存中啊奄,第二個namespace往表中插一條數(shù)據(jù),那么第一個namespace的二級緩存是不會清空這個緩存的內(nèi)容的掀潮,在下一次查詢中菇夸,還會通過緩存去查詢,這樣會造成數(shù)據(jù)的不一致仪吧。

    所以當項目里有多個命名空間操作同一張表的時候庄新,最好不要用二級緩存,或者使用二級緩存時避免用兩個namespace操作一張表薯鼠。

  • Spring整合MyBatis緩存失效問題

    一級緩存的作用域是SqlSession择诈,而使用者可以自定義SqlSession什么時候出現(xiàn)什么時候銷毀,在這段期間一級緩存都是存在的出皇。
    當使用者調(diào)用close()方法之后羞芍,就會銷毀一級緩存。

    但是恶迈,我們在和Spring整合之后涩金,Spring幫我們跳過了SqlSessionFactory這一步谱醇,我們可以直接調(diào)用Mapper,導致在操作完數(shù)據(jù)庫之后步做,Spring就將SqlSession就銷毀了副渴,一級緩存就隨之銷毀了,所以一級緩存就失效了全度。

    那么怎么能讓緩存生效呢煮剧?

    1.開啟事務(wù),因為一旦開啟事務(wù)将鸵,Spring就不會在執(zhí)行完SQL之后就銷毀SqlSession勉盅,因為SqlSession一旦關(guān)閉,事務(wù)就沒了顶掉,一旦我們開啟事務(wù)草娜,在事務(wù)期間內(nèi),緩存會一直存在痒筒。

    2.使用二級緩存宰闰。

結(jié)語

Hello world.

歡迎大家訪問我的個人博客:Object's Blog

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市簿透,隨后出現(xiàn)的幾起案子移袍,更是在濱河造成了極大的恐慌,老刑警劉巖老充,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件葡盗,死亡現(xiàn)場離奇詭異,居然都是意外死亡啡浊,警方通過查閱死者的電腦和手機觅够,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虫啥,“玉大人蔚约,你說我怎么就攤上這事⊥孔眩” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵砸抛,是天一觀的道長评雌。 經(jīng)常有香客問我,道長直焙,這世上最難降的妖魔是什么景东? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮奔誓,結(jié)果婚禮上斤吐,老公的妹妹穿的比我還像新娘搔涝。我一直安慰自己,他們只是感情好和措,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布庄呈。 她就那樣靜靜地躺著,像睡著了一般派阱。 火紅的嫁衣襯著肌膚如雪诬留。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天贫母,我揣著相機與錄音文兑,去河邊找鬼。 笑死腺劣,一個胖子當著我的面吹牛绿贞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播橘原,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼籍铁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了靠柑?” 一聲冷哼從身側(cè)響起寨辩,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎歼冰,沒想到半個月后靡狞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡隔嫡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年甸怕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腮恩。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡梢杭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出秸滴,到底是詐尸還是另有隱情武契,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布荡含,位于F島的核電站咒唆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏释液。R本人自食惡果不足惜全释,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望误债。 院中可真熱鬧浸船,春花似錦妄迁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至项戴,卻和暖如春形帮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背周叮。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工辩撑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人仿耽。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓合冀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親项贺。 傳聞我的和親對象是個殘疾皇子君躺,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容