爲何互聯網大廠都願意用Mybatis?MyBatis緩存特性你瞭解嗎?

爲何互聯網大廠都願意用Mybatis?MyBatis緩存特性你瞭解嗎?

本文已收錄GitHub,更有互聯網大廠面試真題,面試攻略,高效學習資料等java

1、緩存簡介

通常咱們在系統中使用緩存技術是爲了提高數據查詢的效率。當咱們從數據庫中查詢到一批數據後將其放入到混存中(簡單理解就是一塊內存區域),下次再查詢相同數據的時候就直接從緩存中獲取數據就好了。這樣少了一步和數據庫的交互,能夠提高查詢的效率。git

可是一個硬幣都具備兩面性,緩存在帶來性能提高的同時也「悄悄」引入了不少問題,好比緩存同步、緩存失效、緩存雪崩等等。固然這些問題不是本文討論的重點。github

本文主要討論MyBatis緩存這個比較雞肋的功能。雖說MyBatis的緩存功能比較雞肋,可是爲了全面瞭解MyBatis這個框架,學習下緩存這個功能仍是挺有必要的。MyBatis的緩存分爲一級緩存和二級緩存,下面就分別來介紹下這兩個特性。面試

2、一級緩存

在應用運行過程當中,咱們有可能在一次數據庫會話中,執行屢次查詢條件徹底相同的SQL,MyBatis提供了一級緩存的方案優化這部分場景,若是是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提升性能。spring

2.1 什麼是MyBatis一級緩存sql

一級緩存是 SqlSession級別 的緩存。在操做數據庫時須要構造 sqlSession 對象,在對象中有一個(內存區域)數據結構(HashMap)用於存儲緩存數據。不一樣的 sqlSession 之間的緩存數據區域(HashMap)是互相不影響的。數據庫

爲何互聯網大廠都願意用Mybatis?MyBatis緩存特性你瞭解嗎?

在應用運行過程當中,咱們有可能在一次數據庫會話中,執行屢次查詢條件徹底相同的SQL,MyBatis 提供了一級緩存的方案優化這部分場景,若是是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提升性能。緩存

2.2 怎麼開啓一級緩存安全

MyBatis中一級緩存默認是開啓的,不須要咱們作額外的操做。數據結構

若是你須要關閉一級緩存的話,能夠在Mapper映射文件中將flushCache屬性設置爲true,這種作法只會針對單個SQL操做生效

<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap" flushCache="true">
select 
<include refid="Base_Column_List" />
from cbondissuer
where OBJECT_ID = #{objectId,jdbcType=VARCHAR}
</select>

還有一種作法是在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...");

上面進行了四次查詢,若是你觀察日誌的話。會發現只進行了兩個數據庫查詢。由於第二和第三次的查詢都查詢了一級緩存,查出的實際上是緩存中的結果。因此輸出的結果是

cbondissuer10 equals cbondissuer101 :true
cbondissuer10 equals cbondissuer11 :true
cbondissuer10 equals cbondissuer21 :false

2.3 哪些因素會使一級緩存失效

上面的一級緩存初探讓咱們感覺到了 MyBatis 中一級緩存的存在,那麼如今你或許就會有疑問了,那麼何時緩存失效呢?

  • 經過同一個SqlSession執行更新操做時,這個更新操做不只僅指代update操做,還指插入和刪除操做;
  • 事務提交時會刪除一級緩存;
  • 事務回滾時也會刪除一級緩存;

2.4 一級緩存源碼解析

其實MyBatis一級緩存的實質就是一個Executor的一個相似Map的屬性,分析源碼的方法就是看在哪些地方從這個Map中查詢了緩存,又是在哪些清空了這些緩存。

①. 查詢時使用緩存分析

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方法使用了這個緩存:

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語句)。

②. 致使一級緩存失效的代碼分析

查看BaseExecutor的代碼,咱們很容易發現是下面的方法清空了一級緩存。(不要問我是怎麼發現這個代碼的,看代碼能力須要本身慢慢提高)

@Override
public void clearLocalCache() {
    if (!closed) {
        localCache.clear();
        localOutputParameterCache.clear();
    }
}

那麼咱們只要查看哪些地方調用了這個方法就知道哪些狀況下會致使一級緩存失效了。跟蹤下來,最後發現下面三處地方會使得一級緩存失效

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

@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的一級緩存一直沒怎麼被考慮過

@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方法,事務回滾也會致使一級緩存失效。

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

2.5 一級緩存使用建議

平時使用MyBatis時都是和Spring結合使用的,在整個Spring容器中通常只有一個SqlSession實現類。而Spring通常都是主動提交事務的,因此說一級緩存常常失效。

還有就是咱們也不多在一個事務範圍內執行同一個SQL兩遍,上面的這些緣由致使咱們在開發過程當中不多注意到MyBatis一級緩存的存在。

不怎麼用並非說不用,做爲一個合格的開發者須要對這些心知肚明,要清楚的知道MyBatis一級緩存的工做流程。

3、二級緩存

3.1 什麼是MyBatis二級緩存

MyBatis 一級緩存最大的共享範圍就是一個SqlSession內部,那麼若是多個 SqlSession 須要共享緩存,則須要開啓二級緩存,開啓二級緩存後,會使用 CachingExecutor 裝飾 Executor,進入一級緩存的查詢流程前,先在CachingExecutor 進行二級緩存的查詢,具體的工做流程以下所示:

爲何互聯網大廠都願意用Mybatis?MyBatis緩存特性你瞭解嗎?

當二級緩存開啓後,同一個命名空間(namespace) 全部的操做語句,都影響着一個 共同的 cache(一個Mapper映射文件對應一個Cache),也就是二級緩存被多個 SqlSession 共享,是一個全局的變量。當開啓緩存後,數據的查詢執行的流程就是 二級緩存 -> 一級緩存 -> 數據庫。

從上面的圖能夠看出,MyBatis的二級緩存實現能夠有不少種,能夠是MemCache、Ehcache等。也能夠是Redis等,可是須要額外的Jar包。

3.2 怎麼開啓二級緩存

二級緩存默認是不開啓的,須要手動開啓二級緩存,實現二級緩存的時候,MyBatis要求返回的POJO必須是可序列化的。開啓二級緩存的條件也是比較簡單,

step1:經過直接在 MyBatis 配置文件中經過

<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。

3.3 哪些因素會使二級緩存失效

從上面的介紹能夠知道MyBatis的二級緩存主要是爲了SqlSession之間共享緩存設計的。可是咱們平時開發過程當中都是結合Spring來進行MyBatis的開發。在Spring環境下通常也只有一個SqlSession實例,因此二級緩存使用到的機會很少。因此下面就簡單描述下Mybatis的二級緩存。

仍是以上面的列子爲列

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下的所有緩存。
  • 多表操做必定不要使用二級緩存,由於多表操做進行更新操做,必定會產生髒數據。

3.4 二級緩存使用建議

我的以爲MyBatis的二級緩存實用性不是很大。一個緣由就是Spring環境下,一本只有一個SqlSession,不存在sqlSession之間共享緩存;還有就是MyBatis的緩存都不能作到分佈式,因此對於MyBatis的二級緩存以瞭解爲主。

4、簡單總結

4.1 一級緩存

  • 一級緩存的本質是Executor的一個相似Map的屬性;
  • 一級緩存默認開啓,將flushCache設置成true或者將全局配置localCacheScope設置成Statement能夠關閉一級緩存;
  • 在一級緩存開啓的狀況下,查詢操做會先查詢一級緩存,再查詢數據庫;
  • 增刪改操做和事務提交回滾操做會致使一級緩存失效;
  • 因爲Spring中事務是自動提交的,所以Spring下的MyBatis一級緩存常常失效。(可是並不表示不生效,除非你手動關閉一級緩存)
  • 不能實現分佈式。

4.2 二級緩存

  • namesapce級別的緩存(Mapper級別或者叫作表級別的緩存),設計的主要目的是實現sqlSession之間的緩存共享;
  • 開啓二級緩存後,查詢的邏輯是二級緩存->已經緩存->數據庫;
  • insert,update,delete操做會清空所在namespace下的所有緩存;
  • 多表查詢必定不要使用二級緩存,由於多表操做進行更新操做,可能會產生髒數據。
  • 整體來講,MyBatis的緩存功能比較雞肋。想要使用緩存的話仍是建議使用spring-cache等框架。
相關文章
相關標籤/搜索