天天看點

MyBatis緩存特性詳解

MyBatis緩存特性詳解

緩存簡介#

一般我們在系統中使用緩存技術是為了提升資料查詢的效率。當我們從資料庫中查詢到一批資料後将其放入到混存中(簡單了解就是一塊記憶體區域),下次再查詢相同資料的時候就直接從緩存中擷取資料就行了。這樣少了一步和資料庫的互動,可以提升查詢的效率。

但是一個硬币都具有兩面性,緩存在帶來性能提升的同時也“悄悄”引入了很多問題,比如緩存同步、緩存失效、緩存雪崩等等。當然這些問題不是本文讨論的重點。

本文主要讨論MyBatis緩存這個比較雞肋的功能。雖然說MyBatis的緩存功能比較雞肋,但是為了全面了解MyBatis這個架構,學習下緩存這個功能還是挺有必要的。MyBatis的緩存分為一級緩存和二級緩存,下面就分别來介紹下這兩個特性。

一級緩存#

在應用運作過程中,我們有可能在一次資料庫會話中,執行多次查詢條件完全相同的SQL,MyBatis提供了一級緩存的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級緩存,避免直接對資料庫進行查詢,提高性能。

什麼是MyBatis一級緩存#

一級緩存是 SqlSession級别 的緩存。在操作資料庫時需要構造 sqlSession 對象,在對象中有一個(記憶體區域)資料結構(HashMap)用于存儲緩存資料。不同的 sqlSession 之間的緩存資料區域(HashMap)是互相不影響的。

在應用運作過程中,我們有可能在一次資料庫會話中,執行多次查詢條件完全相同的SQL,MyBatis 提供了一級緩存的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級緩存,避免直接對資料庫進行查詢,提高性能。

怎麼開啟一級緩存#

MyBatis中一級緩存預設是開啟的,不需要我們做額外的操作。

如果你需要關閉一級緩存的話,可以在Mapper映射檔案中将flushCache屬性設定為true,這種做法隻會針對單個SQL操作生效

Copy

select 
<include refid="Base_Column_List" />
from cbondissuer
where OBJECT_ID = #{objectId,jdbcType=VARCHAR}           
還有一種做法是在MyBatis的主配置檔案中,關閉所有的一級緩存
預設是SESSION,也就是開啟一級緩存
  <setting name="localCacheScope" value="STATEMENT"/>           

下面我們來寫代碼驗證下MyBatis的一級緩存。

String id = "123";
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//同一個sqlSession建立的Mapper
CbondissuerMapper cbondissuerMapper10 = sqlSession1.getMapper(CbondissuerMapper.class);
CbondissuerMapper cbondissuerMapper11 = sqlSession1.getMapper(CbondissuerMapper.class);
//另外一個sqlSession建立的Mapper
CbondissuerMapper cbondissuerMapper20 = sqlSession2.getMapper(CbondissuerMapper.class);

//同一個Mapper,同樣的SQL查了兩次
Cbondissuer cbondissuer10 = cbondissuerMapper10.selectByPrimaryKey(id);
Cbondissuer cbondissuer101 = cbondissuerMapper10.selectByPrimaryKey(id);
//同一個sqlSession建立的Mapper,又查詢了一次同樣的SQL
Cbondissuer cbondissuer11 = cbondissuerMapper11.selectByPrimaryKey(id);
//不一樣的sqlSession建立的Mapper查詢了一次同樣的SQL
Cbondissuer cbondissuer20 = cbondissuerMapper20.selectByPrimaryKey(id);

System.out.println("cbondissuer10 equals cbondissuer101 :"+(cbondissuer10==cbondissuer101));
System.out.println("cbondissuer10 equals cbondissuer11 :"+(cbondissuer10==cbondissuer11));
System.out.println("cbondissuer10 equals cbondissuer21 :"+(cbondissuer10==cbondissuer20));

sqlSession1.close();
sqlSession2.close();
System.out.println("end...");
上面進行了四次查詢,如果你觀察日志的話。會發現隻進行了兩個資料庫查詢。因為第二和第三次的查詢都查詢了一級緩存,查出的其實是緩存中的結果。是以輸出的結果是

Copy
cbondissuer10 equals cbondissuer101 :true
cbondissuer10 equals cbondissuer11 :true
cbondissuer10 equals cbondissuer21 :false
哪些因素會使一級緩存失效#
上面的一級緩存初探讓我們感受到了 MyBatis 中一級緩存的存在,那麼現在你或許就會有疑問了,那麼什麼時候緩存失效呢?

通過同一個SqlSession執行更新操作時,這個更新操作不僅僅指代update操作,還指插入和删除操作;
事務送出時會删除一級緩存;
事務復原時也會删除一級緩存;
一級緩存源碼解析#
其實MyBatis一級緩存的實質就是一個Executor的一個類似Map的屬性,分析源碼的方法就是看在哪些地方從這個Map中查詢了緩存,又是在哪些清空了這些緩存。

1. 查詢時使用緩存分析

Copy
public abstract class BaseExecutor implements Executor {

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

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  //這個localCache變量就是一級緩存變量
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;
  //..省略下面代碼
}
全局搜尋代碼中哪些地方使用了這個變量,很容易找到BaseExecutor.query方法使用了這個緩存:

Copy
public abstract class BaseExecutor implements Executor {

// 省略其他代碼
 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的query方法使用緩存的過程。需要注意的是查詢緩存時是根據cacheKey進行查詢的,我們可以将這個key簡單的
了解為sql語句,不同的sql語句能查出不同的緩存。(注意sql語句中的參數不同也會被認為是不同的sql語句)。

2. 導緻一級緩存失效的代碼分析
檢視BaseExecutor的代碼,我們很容易發現是下面的方法清空了一級緩存。(不要問我是怎麼發現這個代碼的,看代碼能力需要自己慢慢提升)

Copy
@Override
public void clearLocalCache() {
    if (!closed) {
        localCache.clear();
        localOutputParameterCache.clear();
    }
}
那麼我們隻要檢視哪些地方調用了這個方法就知道哪些情況下會導緻一級緩存失效了。跟蹤下來,最後發現下面三處地方會使得一級緩存失效

BaseExecutor的update方法,使用MyBatis的接口進行增、删、改操作都會調用到這個方法,這個也印證了上面的說法。

Copy
@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);
  }
BaseExecutor的commit方法,事務送出會導緻一級緩存失敗。如果我們使用Spring的話,一般事務都是自動送出的,是以好像MyBatis的一級緩存一直沒怎麼被考慮過

Copy
@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();
    }
  }
BaseExecutor的rollback方法,事務復原也會導緻一級緩存失效。

Copy
@Override
  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }
一級緩存使用建議#
平時使用MyBatis時都是和Spring結合使用的,在整個Spring容器中一般隻有一個SqlSession實作類。而Spring一般都是主動送出事務的,是以說一級緩存經常失效。

還有就是我們也很少在一個事務範圍内執行同一個SQL兩遍,上面的這些原因導緻我們在開發過程中很少注意到MyBatis一級緩存的存在。

不怎麼用并不是說不用,作為一個合格的開發者需要對這些心知肚明,要清楚的知道MyBatis一級緩存的工作流程。

二級緩存#
什麼是MyBatis二級緩存#
MyBatis 一級緩存最大的共享範圍就是一個SqlSession内部,那麼如果多個 SqlSession 需要共享緩存,則需要開啟二級緩存,開啟二級緩存後,會使用 CachingExecutor 裝飾 Executor,
進入一級緩存的查詢流程前,先在CachingExecutor 進行二級緩存的查詢,具體的工作流程如下所示:



當二級緩存開啟後,同一個命名空間(namespace) 所有的操作語句,都影響着一個 共同的 cache(一個Mapper映射檔案對應一個Cache),也就是二級緩存被多個 SqlSession 共享,是一個全局的變量。當開啟緩存後,資料的查詢執行的流程就是 二級緩存 -> 一級緩存 -> 資料庫。

從上面的圖可以看出,MyBatis的二級緩存實作可以有很多種,可以是MemCache、Ehcache等。也可以是Redis等,但是需要額外的Jar包。

怎麼開啟二級緩存#
二級緩存預設是不開啟的,需要手動開啟二級緩存,實作二級緩存的時候,MyBatis要求傳回的POJO必須是可序列化的。開啟二級緩存的條件也是比較簡單,

step1:通過直接在 MyBatis 配置檔案中通過

Copy
<settings>  
    <setting name = "cacheEnabled" value = "true" />
</settings>
step2: 在 Mapper 的xml 配置檔案中加入 标簽

cache标簽下面有下面幾種可選項

eviction: 緩存回收政策,支援的政策有下面幾種
LRU - 最近最少回收,移除最長時間不被使用的對象(預設是這個政策)
FIFO - 先進先出,按照緩存進入的順序來移除它們
SOFT - 軟引用,移除基于垃圾回收器狀态和軟引用規則的對象
WEAK - 弱引用,更積極的移除基于垃圾收集器和弱引用規則的對象
flushinterval:緩存重新整理間隔,緩存多長時間重新整理一次,預設不清空,設定一個毫秒值;
readOnly: 是否隻讀;true 隻讀 ,MyBatis 認為所有從緩存中擷取資料的操作都是隻讀操作,不會修改資料。MyBatis 為了加快擷取資料,直接就會将資料在緩存中的引用交給使用者。不安全,速度快。讀寫(預設):MyBatis 覺得資料可能會被修改
size : 緩存存放多少個元素
type: 指定自定義緩存的全類名(實作Cache 接口即可)
blocking:若緩存中找不到對應的key,是否會一直blocking,直到有對應的資料進入緩存。
cache-ref代表引用别的命名空間的Cache配置,兩個命名空間的操作使用的是同一個Cache。

哪些因素會使二級緩存失效#
從上面的介紹可以知道MyBatis的二級緩存主要是為了SqlSession之間共享緩存設計的。但是我們平時開發過程中都是結合Spring來進行MyBatis的開發。在Spring環境下一般也隻有一個SqlSession執行個體,是以二級緩存使用到的機會不多。是以下面就簡單描述下Mybatis的二級緩存。

還是以上面的列子為列

Copy
String id = "{0003CCCA-AEA9-4A1E-A3CC-06D884BA3906}";
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//同一個sqlSession建立的Mapper
CbondissuerMapper cbondissuerMapper10 = sqlSession1.getMapper(CbondissuerMapper.class);
CbondissuerMapper cbondissuerMapper11 = sqlSession1.getMapper(CbondissuerMapper.class);
//另外一個sqlSession建立的Mapper
CbondissuerMapper cbondissuerMapper20 = sqlSession2.getMapper(CbondissuerMapper.class);

//同一個Mapper,同樣的SQL查了兩次
Cbondissuer cbondissuer10 = cbondissuerMapper10.selectByPrimaryKey(id);
Cbondissuer cbondissuer101 = cbondissuerMapper10.selectByPrimaryKey(id);
//同一個sqlSession建立的Mapper,又查詢了一次同樣的SQL
Cbondissuer cbondissuer11 = cbondissuerMapper11.selectByPrimaryKey(id);
//這邊需要送出事務才能讓二級緩存生效
sqlSession1.commit();
//不一樣的sqlSession建立的Mapper查詢了一次同樣的SQL
Cbondissuer cbondissuer20 = cbondissuerMapper20.selectByPrimaryKey(id);

System.out.println("cbondissuer10 equals cbondissuer101 :"+(cbondissuer10==cbondissuer101));
System.out.println("cbondissuer10 equals cbondissuer11 :"+(cbondissuer10==cbondissuer11));
System.out.println("cbondissuer10 equals cbondissuer21 :"+(cbondissuer10==cbondissuer20));
二級緩存是以namespace(Mapper)為機關的,不同namespace下的操作互不影響。
insert,update,delete操作會清空所在namespace下的全部緩存。
多表操作一定不要使用二級緩存,因為多表操作進行更新操作,一定會産生髒資料。
二級緩存使用建議#
個人覺得MyBatis的二級緩存實用性不是很大。一個原因就是Spring環境下,一本隻有一個SqlSession,不存在sqlSession之間共享緩存;還有就是
MyBatis的緩存都不能做到分布式,是以對于MyBatis的二級緩存以了解為主。

簡單總結#
一級緩存#
一級緩存的本質是Executor的一個類似Map的屬性;
一級緩存預設開啟,将flushCache設定成true或者将全局配置localCacheScope設定成Statement可以關閉一級緩存;
在一級緩存開啟的情況下,查詢操作會先查詢一級緩存,再查詢資料庫;
增删改操作和事務送出復原操作會導緻一級緩存失效;
由于Spring中事務是自動送出的,是以Spring下的MyBatis一級緩存經常失效。(但是并不表示不生效,除非你手動關閉一級緩存)
不能實作分布式。
二級緩存#
namesapce級别的緩存(Mapper級别或者叫做表級别的緩存),設計的主要目的是實作sqlSession之間的緩存共享;
開啟二級緩存後,查詢的邏輯是二級緩存->已經緩存->資料庫;
insert,update,delete操作會清空所在namespace下的全部緩存;
多表查詢一定不要使用二級緩存,因為多表操作進行更新操作,可能會産生髒資料。
總體來說,MyBatis的緩存功能比較雞肋。想要使用緩存的話還是建議使用spring-cache等架構。

參考#
https://blog.csdn.net/zb313982521/article/details/79689169
https://mp.weixin.qq.com/s?__biz=MzI4NDY5Mjc1Mg==&mid=2247489120&idx=2&sn=4694c4a359849d17354f85206768c25b&chksm=ebf6ce1fdc81470918515ff76c41d7aea9434226ef05e930fec59ed22dcc709030a6683c0d80&mpshare=1&scene=1&srcid=&sharer_sharetime=1566873637232&sharer_shareid=2040c1b4c62e1f430c804ebd0fe79fa3#rd
作者: 程式員自由之路