<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入淺出 MyBatis 的一級、二級緩存機制

          共 23628字,需瀏覽 48分鐘

           ·

          2021-07-13 02:19

          點擊上方老周聊架構(gòu)關(guān)注我


          一、MyBatis 緩存

          緩存就是內(nèi)存中的數(shù)據(jù),常常來自對數(shù)據(jù)庫查詢結(jié)果的保存。使用緩存,我們可以避免頻繁與數(shù)據(jù)庫進行交互,從而提高響應(yīng)速度。

          MyBatis 也提供了對緩存的支持,分為一級緩存和二級緩存,來看下下面這張圖:


          一級緩存是 SqlSession 級別的緩存。在操作數(shù)據(jù)庫時需要構(gòu)造 SqlSession 對象,在對象中有一個數(shù)據(jù)結(jié)構(gòu)(HashMap)用于存儲緩存數(shù)據(jù)。不同的是 SqlSession 之間的緩存數(shù)據(jù)區(qū)(HashMap)是互相不影響。

          二級緩存是 Mapper 級別的緩存,多個 SqlSession 去操作同一個 Mapper 的 sql 語句,多個 SqlSession 可以共用二級緩存,二級緩存是跨 SqlSession 的。

          相信大家看完這張圖和解釋心里應(yīng)該有個底了吧,這對后面分析 MyBatis 的一級、二級緩存機制很有幫助,那話不多說,我們直接進入主題了。

          二、一級緩存

          2.1 內(nèi)部結(jié)構(gòu)

          在我們的應(yīng)用運行期間,我們可能在一次數(shù)據(jù)庫會話中,執(zhí)行多次查詢條件相同的 SQL,要你來設(shè)計的話你會如何考慮?沒錯,加緩存,MyBatis 也是這樣去處理的,如果是相同的 SQL 語句,會優(yōu)先命中一級緩存,避免直接對數(shù)據(jù)庫進行查詢,造成數(shù)據(jù)庫的壓力,以提高性能。具體執(zhí)行過程如下圖所示:

          SqlSession 是一個接口,提供了一些 CRUD 的方法,而 SqlSession 的默認實現(xiàn)類是 DefaultSqlSession,DefaultSqlSession 類持有 Executor 接口對象,而 Executor 的默認實現(xiàn)是 BaseExecutor 對象,每個 BaseExecutor 對象都有一個 PerpetualCache 緩存,也就是上圖的  Local Cache。

          當用戶發(fā)起查詢時,MyBatis 根據(jù)當前執(zhí)行的語句生成 MappedStatement,在 Local Cache 進行查詢,如果緩存命中的話,直接返回結(jié)果給用戶,如果緩存沒有命中的話,查詢數(shù)據(jù)庫,結(jié)果寫入 Local Cache,最后返回結(jié)果給用戶。

          啊,老周,關(guān)系還是有點抽象,感覺一直在套娃,沒關(guān)系,看下面這張圖你立馬豁然開朗。


          2.2 一級緩存配置

          在 MyBatis 的配置文件中添加如下語句,就可以使用一級緩存。共有兩個選項,SESSION 或者 STATEMENT,默認是 SESSION 級別,即在一個 MyBatis 會話中執(zhí)行的所有語句,都會共享這一個緩存。一種是 STATEMENT 級別,可以理解為緩存只對當前執(zhí)行的這一個 Statement 有效。

          STATEMENT 級別粒度更細,我們上面說到,每個 SqlSession 中持有了 Executor,SqlSession 的默認實現(xiàn)類是 DefaultSqlSession,DefaultSqlSession 類持有 Executor 接口對象,而 Executor 的默認實現(xiàn)是 BaseExecutor 對象,每個 BaseExecutor 對象很多方法中都有傳 MappedStatement 對象。所有 STATEMENT 級別是針對 SESSION 級別粒度更細的模式。

          <setting name="localCacheScope" value="SESSION"/>

          三、一級緩存實驗

          下面老周通過幾組實驗來帶你了解 MyBatis 一級緩存的效果,我們首先準備一張簡單的表 user,如下:

          CREATE TABLE `user` (
            `id` int(11unsigned NOT NULL AUTO_INCREMENT,
            `name` varchar(64COLLATE utf8_bin DEFAULT NULL,
            PRIMARY KEY (`id`)
          ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

          我們在測試類中加上帶有 @Before 標注的 before 方法,省得每個單元測試方法都要重復(fù)獲取 sqlSession 以及 userMapper。

          @Before
          public void before() throws IOException {
              InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
              sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
              sqlSession = sqlSessionFactory.openSession(true); // 自動提交事務(wù)
              userMapper = sqlSession.getMapper(UserMapper.class);
          }

          3.1 實驗1

          開啟一級緩存,范圍為會話級別,調(diào)用三次 firstLevelCacheFindUserById,代碼如下所示:

          @Test
          public void firstLevelCacheFindUserById() {
              // 第一次查詢id為1的用戶
              User user1 = userMapper.findUserById(1);
              // 第二次查詢id為1的用戶
              User user2 = userMapper.findUserById(1);

              System.out.println(user1);
              System.out.println(user2);

              System.out.println(user1 == user2);
          }

          控制臺日志輸出:


          我們可以看到,只有第一次真正查詢了數(shù)據(jù)庫,后續(xù)的查詢使用了一級緩存。

          3.2 實驗2

          增加了對數(shù)據(jù)庫的修改操作,驗證在一次數(shù)據(jù)庫會話中,如果對數(shù)據(jù)庫發(fā)生了修改操作,一級緩存是否會失效。

          @Test
          public void firstLevelCacheOfUpdate() {
              // 第一次查詢id為1的用戶
              User user1 = userMapper.findUserById(1);
              System.out.println(user1);

              // 更新用戶
              User user = new User();
              user.setId(2);
              user.setUsername("tom");

              System.out.println("更新了" + userMapper.updateUser(user) + "個用戶");

              // 第二次查詢id為1的用戶
              User user2 = userMapper.findUserById(1);
              System.out.println(user2);

              System.out.println(user1 == user2);
          }

          控制臺日志輸出:


          我們可以看到,在修改操作后執(zhí)行的相同查詢,查詢了數(shù)據(jù)庫,一級緩存失效。

          3.3 實驗3

          開啟兩個 SqlSession,在 sqlSession1 中查詢數(shù)據(jù),使一級緩存生效,在 sqlSession2 中更新數(shù)據(jù)庫,驗證一級緩存只在數(shù)據(jù)庫會話內(nèi)部共享。

          @Test
          public void firstLevelCacheOfScope() {
              SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
              UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

              System.out.println("userMapper讀取數(shù)據(jù): " + userMapper.findUserById(1));
              System.out.println("userMapper讀取數(shù)據(jù): " + userMapper.findUserById(1));

              // 更新用戶
              User user = new User();
              user.setId(1);
              user.setUsername("andy");
              System.out.println("userMapper2更新了" + userMapper2.updateUser(user) + "個用戶");

              System.out.println("userMapper讀取數(shù)據(jù): " + userMapper.findUserById(1));
              System.out.println("userMapper2讀取數(shù)據(jù): " + userMapper2.findUserById(1));
          }

          控制臺日志輸出:


          sqlSession2 更新了 id 為 1 的用戶的姓名,從 riemann 改為了 andy,但 session1 之后的查詢中,id 為 1 的學(xué)生的名字還是 riemann,出現(xiàn)了臟數(shù)據(jù),也證明了之前的設(shè)想,一級緩存只在數(shù)據(jù)庫會話內(nèi)部共享。

          四、一級緩存工作流程以及源碼分析

          4.1 工作流程

          我們來看下一級緩存的時序圖:


          4.2 源碼分析

          看完上面的整體時序流程,我相信大家基本框架了解了,接下來針對這個框架再進行源碼細化走讀。

          SqlSession:對外提供了用戶和數(shù)據(jù)庫之間交互需要的所有方法,隱藏了底層的細節(jié)。默認實現(xiàn)類是 DefaultSqlSession。


          Executor:SqlSession 向用戶提供操作數(shù)據(jù)庫的方法,但和數(shù)據(jù)庫操作有關(guān)的職責都會委托給 Executor。


          如下圖所示,Executor 有若干個實現(xiàn)類,為 Executor 賦予了不同的能力。


          在一級緩存的源碼分析中,主要學(xué)習 BaseExecutor 的內(nèi)部實現(xiàn)。

          BaseExecutor:BaseExecutor 是一個實現(xiàn)了 Executor 接口的抽象類,定義若干抽象方法,在執(zhí)行的時候,把具體的操作委托給子類進行執(zhí)行。

          protected abstract int doUpdate(MappedStatement var1, Object var2) throws SQLException;
          protected abstract List<BatchResult> doFlushStatements(boolean var1) throws SQLException;
          protected abstract <E> List<E> doQuery(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, BoundSql var5) throws SQLException;
          protected abstract <E> Cursor<E> doQueryCursor(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4) throws SQLException;

          在上文有提到對 Local Cache 的查詢和寫入是在 Executor 內(nèi)部完成的。在閱讀 BaseExecutor 的代碼后發(fā)現(xiàn) Local Cache 是 BaseExecutor 內(nèi)部的一個成員變量,如下代碼所示。

          public abstract class BaseExecutor implements Executor {
              ...
              protected ConcurrentLinkedQueue<BaseExecutor.DeferredLoad> deferredLoads;
              protected PerpetualCache localCache;
              protected PerpetualCache localOutputParameterCache;
              ...
          }

          Cache:MyBatis 中的 Cache 接口,提供了和緩存相關(guān)的最基本的操作,如下圖所示:


          有若干個實現(xiàn)類,使用裝飾器模式互相組裝,提供豐富的操控緩存的能力,部分實現(xiàn)類如下圖所示:


          BaseExecutor 成員變量之一的 PerpetualCache,是對 Cache 接口最基本的實現(xiàn),其實現(xiàn)非常簡單,內(nèi)部持有 HashMap,對一級緩存的操作實則是對 HashMap 的操作。如下代碼所示:

          public class PerpetualCache implements Cache {
              private final String id;
              private Map<Object, Object> cache = new HashMap();
              ...
          }

          調(diào)研了一下,畫出工作流程圖:


          跟蹤到 PerpetualCache 中的 clear() 方法。

          public class PerpetualCache implements Cache {
              ...
              private Map<Object, Object> cache = new HashMap();

              public void clear() {
                  this.cache.clear();
              }
              ...
          }

          也就是說一級緩存的底層數(shù)據(jù)結(jié)構(gòu)就是 HashMap。所以說 cache.clear() 其實就是 map.clear(),也就是說,緩存其實是本地存放的一個 map 對象,每一個 SqlSession 都會存放一個 map 對象的引用。

          那么這個 cache 是何時創(chuàng)建的呢?

          根據(jù)上面我們畫的工作流程,明顯在 Executor 執(zhí)行器,執(zhí)行器用來執(zhí)行 sql 請求,而且清除緩存的方法也在 Executor 中執(zhí)行,去查看源碼,果真在里面。


          創(chuàng)建緩存 key 會經(jīng)過一系列的 update 方法,update 方法由一個 cacheKey 這個對象來執(zhí)行的。這個 update 方法最終由 updateList 的 list 來把六個值存進去,對照上面的代碼,你應(yīng)該能理解下面六個值分別是啥了吧。


          這里需要關(guān)注最后一個值 this.configuration.getEnvironment().getId(),這其實就是定義在 mybatis-config.xml 中的標簽。如下:


          那么問題來了,創(chuàng)建緩存了,那具體在哪里用呢?我們一級緩存探究后,我們發(fā)現(xiàn)一級緩存更多的用于查詢操作。我們跟蹤到 query 方法:


          如果查不到的話,就從數(shù)據(jù)庫查,在 queryFromDatabase 中,會對 localcache 進行寫入。

          在 query 方法執(zhí)行的最后,會判斷一級緩存級別是否是 STATEMENT 級別,如果是的話,就清空緩存,這也就是 STATEMENT 級別的一級緩存無法共享 localCache 的原因。代碼如下所示:

          if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
              clearLocalCache();
          }

          在源碼分析的最后,我們確認一下,如果是 insert/delete/update 方法,緩存就會刷新的原因。

          SqlSession 的 insert 方法和 delete 方法,都會統(tǒng)一走 update 的流程,代碼如下所示:

          @Override
          public int insert(String statement, Object parameter) {
              return update(statement, parameter);
          }

          @Override
          public int delete(String statement) {
              return update(statement, null);
          }

          update 方法也是委托給了 Executor 執(zhí)行。BaseExecutor 的執(zhí)行方法如下所示:

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

          每次執(zhí)行 update 前都會清空 localCache。

          至此,一級緩存的工作流程講解以及源碼分析完畢。

          五、一級緩存小結(jié)

          • MyBatis 一級緩存的生命周期和 SqlSession 一致。

          • MyBatis 一級緩存內(nèi)部設(shè)計簡單,只是一個沒有容量限定的 HashMap,在緩存的功能性上有所欠缺。

          • MyBatis 的一級緩存最大范圍是 SqlSession 內(nèi)部,有多個 SqlSession 或者分布式的環(huán)境下,數(shù)據(jù)庫寫操作會引起臟數(shù)據(jù),建議設(shè)定緩存級別為 Statement。

          六、二級緩存

          在上文中提到的一級緩存中,其最大的共享范圍就是一個 SqlSession 內(nèi)部,如果多個 SqlSession 之間需要共享緩存,則需要使用到二級緩存。開啟二級緩存后,會使用 CachingExecutor 裝飾 Executor,進入一級緩存的查詢流程前,先在 CachingExecutor 進行二級緩存的查詢,具體的工作流程如下所示。


          二級緩存開啟后,同一個 namespace 下的所有操作語句,都影響著同一個 Cache,即二級緩存被多個 SqlSession 共享,是一個全局的變量。

          當開啟緩存后,數(shù)據(jù)的查詢執(zhí)行的流程就是 二級緩存 -> 一級緩存 -> 數(shù)據(jù)庫。

          MyBatis 是默認關(guān)閉二級緩存的,因為對于增刪改操作頻繁的話,那么二級緩存形同虛設(shè),每次都會被清空緩存。

          6.1 二級緩存配置

          和一級緩存默認開啟不一樣,二級緩存需要我們手動開啟。

          6.1.1 首先在全局配置文件 SqlMapConfig.xml 文件中加入如下代碼:

          <!--開啟二級緩存-->
          <settings>
              <setting name="cacheEnabled" value="true"/>
          </settings>

          6.1.2 其次在 UserMapper.xml 文件中開啟二級緩存

          mapper 代理模式:

          <!--開啟二級緩存-->
          <cache />

          注解開發(fā)模式:

          @CacheNamespace(implementation = PerpetualCache.class) // 開啟二級緩存
          public interface UserMapper {
          }

          mapper 代理模式開啟的二級緩存是一個空標簽,其實這里可以配置,PerpetualCache 這個類是 mybatis 默認實現(xiàn)的二級緩存功能的類,我們不寫 type ,用 @CacheNamespace 直接默認 PerpetualCache 這個類,也可以去實現(xiàn) Cache 接口來自定義緩存。

          6.2 實體類實現(xiàn) Serializable 序列化接口


          開啟二級緩存后,還需要將要緩存的實體類去實現(xiàn) Serializable 序列化接口,為了將緩存數(shù)據(jù)取出執(zhí)行反序列化操作,因為二級緩存數(shù)據(jù)存儲介質(zhì)多種多樣,不一定只存在內(nèi)存中,有可能存在硬盤中,如果我們再取出這個緩存的話,就需要反序列化。所以 MyBatis 的所有 pojo 類都要去實現(xiàn) Serializable 序列化接口。

          七、二級緩存實驗

          7.1 實驗1

          測試二級緩存與 SqlSession 無關(guān)

          @Test
          public void secondLevelCache() {
              SqlSession sqlSession1 = sqlSessionFactory.openSession();
              SqlSession sqlSession2 = sqlSessionFactory.openSession();

              UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
              UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

              // 第一次查詢id為1的用戶
              User user1 = userMapper1.findUserById(1);
              sqlSession1.close(); // 清空一級緩存
              System.out.println(user1);

              // 第二次查詢id為1的用戶
              User user2 = userMapper2.findUserById(1);
              System.out.println(user2);

              System.out.println(user1 == user2);
          }

          控制臺日志輸出:


          第一次查詢時,將查詢結(jié)果放入緩存中,第二次查詢,即使 sqlSession1.close(); 清空了一級緩存,第二次查詢依然不發(fā)出 sql 語句。

          這里的你可能有個疑問,這里不是二級緩存了嗎?怎么 user1 與 user2 不相等?

          這是因為二級緩存的是數(shù)據(jù),并不是對象。而 user1 與 user2 是兩個對象,所以地址值當然也不想等。

          7.2 實驗2

          測試執(zhí)行 commit(),二級緩存數(shù)據(jù)清空。

          @Test
          public void secondLevelCacheOfUpdate() {
              SqlSession sqlSession1 = sqlSessionFactory.openSession();
              SqlSession sqlSession2 = sqlSessionFactory.openSession();
              SqlSession sqlSession3 = sqlSessionFactory.openSession();

              UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
              UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
              UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);

              // 第一次查詢id為1的用戶
              User user1 = userMapper1.findUserById(1);
              sqlSession1.close(); // 清空一級緩存

              User user = new User();
              user.setId(3);
              user.setUsername("edgar");
              userMapper3.updateUser(user);
              sqlSession3.commit();

              // 第二次查詢id為1的用戶
              User user2 = userMapper2.findUserById(1);
              sqlSession2.close();

              System.out.println(user1 == user2);
          }

          控制臺日志輸出:


          我們可以看到,在 sqlSession3 更新數(shù)據(jù)庫,并提交事務(wù)后,sqlsession2 的 UserMapper namespace 下的查詢走了數(shù)據(jù)庫,沒有走 Cache。

          7.3 實驗3

          驗證 MyBatis 的二級緩存不適應(yīng)用于映射文件中存在多表查詢的情況。

          通常我們會為每個單表創(chuàng)建單獨的映射文件,由于 MyBatis 的二級緩存是基于 namespace 的,多表查詢語句所在的 namspace 無法感應(yīng)到其他 namespace 中的語句對多表查詢中涉及的表進行的修改,引發(fā)臟數(shù)據(jù)問題。

          為了解決實驗3的問題呢,可以使用 Cache ref,讓 OrderMapper 引用 UserMapper 命名空間,這樣兩個映射文件對應(yīng)的 SQL 操作都使用的是同一塊緩存了。

          不過這樣做的后果是,緩存的粒度變粗了,多個 Mapper namespace 下的所有操作都會對緩存使用造成影響。

          這里老周就不代碼演示了,有沒有感覺很雞肋,而且不熟用二級緩存的話,像這種多表查詢的,很容易造成臟讀數(shù)據(jù)不一致,這在線上的話是致命的。

          7.4 useCache 和 flushCache

          useCache 是用來設(shè)置是否禁用二級緩存的,在 statement 中設(shè)置 useCache="false",可以禁用當前 select 語句的二級緩存,即每次都會去數(shù)據(jù)庫查詢。如下:

          <select id="findAll" resultMap="userMap" useCache="false">
              select * from user u left join orders o on u.id = o.uid
          </select>

          設(shè)置 statement 配置中的 flushCache=“true” 屬性,默認情況下為 true,即刷新緩存,一般執(zhí)行完 commit 操作都需要刷新緩存,flushCache=“true” 表示刷新緩存,這樣可以避免增刪改操作而導(dǎo)致的臟讀問題。默認不要配置,如下:

          <select id="findAll" resultMap="userMap" useCache="false" flushCache="true">
              select * from user u left join orders o on u.id = o.uid
          </select>

          八、二級緩存源碼分析

          MyBatis 二級緩存的工作流程和前文提到的一級緩存類似,只是在一級緩存處理前,用 CachingExecutor 裝飾了 BaseExecutor 的子類,在委托具體職責給 delegate 之前,實現(xiàn)了二級緩存的查詢和寫入功能,具體類關(guān)系圖如下圖所示。


          源碼分析從 CachingExecutor 的 query 方法展開,首先會從 MappedStatement 中獲得在配置初始化時賦予的 Cache。

          public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
              Cache cache = ms.getCache(); // 首先會從 MappedStatement 中獲得在配置初始化時賦予的 Cache
              if (cache != null) {
                  this.flushCacheIfRequired(ms);
                  if (ms.isUseCache() && resultHandler == null) {
                      this.ensureNoOutParams(ms, parameterObject, boundSql);
                      List<E> list = (List)this.tcm.getObject(cache, key);
                      if (list == null) {
                          list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                          this.tcm.putObject(cache, key, list);
                      }

                      return list;
                  }
              }

              return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          }

          本質(zhì)上是裝飾器模式的使用,具體的裝飾鏈是:

          SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。


          以下是具體這些 Cache 實現(xiàn)類的介紹,他們的組合為 Cache 賦予了不同的能力。

          • SynchronizedCache:同步 Cache,實現(xiàn)比較簡單,直接使用 synchronized 修飾方法。

          • LoggingCache:日志功能,裝飾類,用于記錄緩存的命中率,如果開啟了 DEBUG 模式,則會輸出命中率日志。

          • SerializedCache:序列化功能,將值序列化后存到緩存中。該功能用于緩存返回一份實例的 Copy,用于保存線程安全。

          • LruCache:采用了 LRU 算法的 Cache 實現(xiàn),移除最近最少使用的 Key/Value。

          • PerpetualCache:作為為最基礎(chǔ)的緩存類,底層實現(xiàn)比較簡單,直接使用了 HashMap。

          然后是判斷是否需要刷新緩存,也就是上面代碼中的:

          this.flushCacheIfRequired(ms);

          在默認的設(shè)置中 SELECT 語句不會刷新緩存,insert/update/delte 會刷新緩存。進入該方法。代碼如下所示:

          private void flushCacheIfRequired(MappedStatement ms) {
              Cache cache = ms.getCache();
              if (cache != null && ms.isFlushCacheRequired()) {
                  this.tcm.clear(cache);
              }
          }

          MyBatis 的 CachingExecutor 持有了 TransactionalCacheManager,即上述代碼中的 tcm。

          TransactionalCacheManager 中持有了一個 Map,代碼如下所示:

          private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

          這個 Map 保存了 Cache 和用 TransactionalCache 包裝后的 Cache 的映射關(guān)系。

          TransactionalCache 實現(xiàn)了 Cache 接口,CachingExecutor 會默認使用他包裝初始生成的 Cache,作用是如果事務(wù)提交,對緩存的操作才會生效,如果事務(wù)回滾或者不提交事務(wù),則不對緩存產(chǎn)生影響。

          在 TransactionalCache 的 clear,有以下兩句。清空了需要在提交時加入緩存的列表,同時設(shè)定提交時清空緩存,代碼如下所示:

          @Override
          public void clear() {
              clearOnCommit = true;
              entriesToAddOnCommit.clear();
          }

          CachingExecutor#query 繼續(xù)往下走,ensureNoOutParams 主要是用來處理存儲過程的,暫時不用考慮。

          if (ms.isUseCache() && resultHandler == null) {
              ensureNoOutParams(ms, parameterObject, boundSql);
              ...
          }

          之后會嘗試從tcm中獲取緩存的列表。

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

          在 getObject 方法中,會把獲取值的職責一路傳遞,最終到 PerpetualCache。如果沒有查到,會把 key 加入 Miss 集合,這個主要是為了統(tǒng)計命中率。

          // TransactionalCache#getObject
          public Object getObject(Object key) {
              Object object = this.delegate.getObject(key);
              if (object == null) {
                  this.entriesMissedInCache.add(key);
              }

              return this.clearOnCommit ? null : object;
          }

          CachingExecutor 繼續(xù)往下走,如果查詢到數(shù)據(jù),則調(diào)用 tcm.putObject 方法,往緩存中放入值。

          if (list == null) {
              list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
              this.tcm.putObject(cache, key, list); // issue #578 and #116
          }

          tcm 的 put 方法也不是直接操作緩存,只是在把這次的數(shù)據(jù)和 key 放入待提交的 Map 中。

          public void putObject(Cache cache, CacheKey key, Object value) {
              this.getTransactionalCache(cache).putObject(key, value);
          }

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

          從以上的代碼分析中,我們可以明白,如果不調(diào)用 commit 方法的話,由于 TranscationalCache 的作用,并不會對二級緩存造成直接的影響。我們來看下 CachingExecutor#commit 方法:

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

          會把具體 commit 的職責委托給包裝的 Executor。主要是看下tcm.commit(),tcm 最終又會調(diào)用到TrancationalCache。

          // TransactionalCacheManager#commit
          public void commit() {
              Iterator var1 = this.transactionalCaches.values().iterator();

              while(var1.hasNext()) {
                  TransactionalCache txCache = (TransactionalCache)var1.next();
                  txCache.commit();
              }

          }

          // TransactionalCache#commit
          public void commit() {
              if (this.clearOnCommit) {
                  this.delegate.clear();
              }

              this.flushPendingEntries();
              this.reset();
          }

          看到這里的 clearOnCommit 就想起剛才 TrancationalCache 的 clear 方法設(shè)置的標志位,真正的清理 Cache 是放到這里來進行的。具體清理的職責委托給了包裝的 Cache 類。之后進入 flushPendingEntries 方法。代碼如下所示:

          private void flushPendingEntries() {
              Iterator var1 = this.entriesToAddOnCommit.entrySet().iterator();

              while(var1.hasNext()) {
                  Entry<Object, Object> entry = (Entry)var1.next();
                  this.delegate.putObject(entry.getKey(), entry.getValue());
              }

              var1 = this.entriesMissedInCache.iterator();

              while(var1.hasNext()) {
                  Object entry = var1.next();
                  if (!this.entriesToAddOnCommit.containsKey(entry)) {
                      this.delegate.putObject(entry, (Object)null);
                  }
              }
          }

          在 flushPendingEntries 中,將待提交的 Map 進行循環(huán)處理,委托給包裝的 Cache 類,進行 putObject 的操作。

          后續(xù)的查詢操作會重復(fù)執(zhí)行這套流程。如果是 insert|update|delete 的話,會統(tǒng)一進入 CachingExecutor 的 update 方法,其中調(diào)用了這個函數(shù),代碼如下所示:

          private void flushCacheIfRequired(MappedStatement ms) 

          在二級緩存執(zhí)行流程后就會進入一級緩存的執(zhí)行流程,因此不再贅述  。

          九、二級緩存小結(jié)

          • MyBatis 的二級緩存相對于一級緩存來說,實現(xiàn)了 SqlSession 之間緩存數(shù)據(jù)的共享,同時粒度更加的細,能夠到 namespace 級別,通過 Cache 接口實現(xiàn)類不同的組合,對 Cache 的可控性也更強。

          • MyBatis 在多表查詢時,極大可能會出現(xiàn)臟數(shù)據(jù),有設(shè)計上的缺陷,安全使用二級緩存的條件比較苛刻。

          • 在分布式環(huán)境下,由于默認的 MyBatis Cache 實現(xiàn)都是基于本地的,分布式環(huán)境下必然會出現(xiàn)讀取到臟數(shù)據(jù),需要使用集中式緩存將 MyBatis 的 Cache 接口實現(xiàn),有一定的開發(fā)成本,直接使用 Redis、Memcached 等分布式緩存可能成本更低,安全性也更高。

          十、總結(jié)

          本文先是介紹了 MyBatis 的緩存,MyBatis 的緩存分為一、二級緩存,一級緩存是 SqlSession 級別的緩存,二級緩存是 Mapper 級別的緩存;然后從工作流程、應(yīng)用試驗以及源碼層面分析了 MyBatis 的一、二級緩存機制;最后對 MyBatis 的一、二級緩存做了相應(yīng)的小結(jié)。

          老周建議 MyBatis 的一級、二級緩存只作為 ORM 框架使用就行了,線上環(huán)境得關(guān)閉 MyBatis 的緩存機制。通過全文分析,不知道你有沒有覺得 MyBatis 的緩存機制很雞肋?

          一級緩存來說對于有多個 SqlSession 或者分布式的環(huán)境下,數(shù)據(jù)庫寫操作會引起臟數(shù)據(jù)以及對于增刪改多的操作來說,清除一級緩存會很頻繁,這會導(dǎo)致一級緩存形同虛設(shè)。

          二級緩存來說實現(xiàn)了 SqlSession 之間緩存數(shù)據(jù)的共享,除了跟一級緩存一樣對于增刪改多的操作來說,清除二級緩存會很頻繁,這會導(dǎo)致二級緩存形同虛設(shè);MyBatis 的二級緩存不適應(yīng)用于映射文件中存在多表查詢的情況,由于 MyBatis 的二級緩存是基于 namespace 的,多表查詢語句所在的 namspace 無法感應(yīng)到其他 namespace 中的語句對多表查詢中涉及的表進行的修改,引發(fā)臟數(shù)據(jù)問題。雖然可以通過 Cache ref 來解決多表的問題,但這樣做的后果是,緩存的粒度變粗了,多個 Mapper namespace 下的所有操作都會對緩存使用造成影響。

          綜上,生產(chǎn)環(huán)境要關(guān)閉 MyBatis 的緩存機制。你可能會問,老周,你說生產(chǎn)環(huán)境不推薦用,那為啥很多面試官很喜歡問 MyBatis 的一級、二級緩存機制呢?那你把老周這篇丟給他就好了,最后你再反問面試官,你們生產(chǎn)環(huán)境有用 MyBatis 的一級、二級緩存機制嗎?大多數(shù)的答案要么是沒用或者它自己也不知道用沒用就隨便那幾道題來面你。如果面試官回答生產(chǎn)環(huán)境用了的話,那你就把這些用的弊端跟面試官交流交流。

          好了深入淺出 MyBatis 的一級、二級緩存機制就到這了,我們下期再見。



          歡迎大家關(guān)注我的公眾號【老周聊架構(gòu)】,Java后端主流技術(shù)棧的原理、源碼分析、架構(gòu)以及各種互聯(lián)網(wǎng)高并發(fā)、高性能、高可用的解決方案。

          喜歡的話,點贊、再看、分享三連。

          點個在看你最好看

          瀏覽 101
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产成人精品电影 | 欧美怡红院视频 | 囯产一级a一级a免费视频 | 操女人的小骚逼被操舒服视频 | 在线国产福利视频 |