MyBatis 一級緩存和二級緩存存在的問題和原理源碼介紹

版本:mybatis-3.5.4

mybatis的單元測試採用HSQLDB

HSQLDB官方文檔:http://hsqldb.org/doc/2.0/guide/index.html。

 

緩存是MyBatis中非常重要的特性。在應用程序和數據庫都是單節點的情況下,合理使用緩存能夠減少數據庫IO,顯著提升系統性能。但是在分佈式環境下,如果使用不當,則可能會帶來數據一致性問題。MyBatis提供了一級緩存和二級緩存,其中一級緩存基於SqlSession實現,而二級緩存基於Mapper實現。

MyBatis 中兩級緩存都是依賴於基礎支持層中的緩存模塊實現的。MyBatis 中自帶的這兩級緩存與 MyBatis 以及整個應用是運行在同一個 JVM 中的,共享同一塊堆內存。如果這兩級緩存中的數據量較大, 則可能影響系統中其他功能的運行,所以當需要緩存大量數據時,優先考慮使用 Redis、Memcache 等緩存產品。

看完文章,你將瞭解以下問題:

1、mybatis一級緩存爲什麼不能關閉?對數據一致性要求比較高的場景如果做?

2、爲什麼dao層只有接口沒有實現類?

 

講緩存之前,先簡單講一下mybatis組件構成:

mybatis組件介紹

 

  • Configuration:用於描述MyBatis的主配置信息,其他組件需要獲取配置信息時,直接通過Configuration對象獲取。除此之外,MyBatis在應用啓動時,將Mapper配置信息、類型別名、TypeHandler等註冊到Configuration組件中,其他組件需要這些信息時,也可以從Configuration對象中獲取。
  • MappedStatement:MappedStatement用於描述Mapper中的SQL配置信息,是對Mapper XML配置文件中<select|update|delete|insert>等標籤或者@Select/@Update等註解配置信息的封裝。
  • SqlSession:SqlSession是MyBatis提供的面向用戶的API,表示和數據庫交互時的會話對象,用於完成數據庫的增刪改查功能。SqlSession是Executor組件的外觀,目的是對外提供易於理解和使用的數據庫操作接口。Spring事物和SqlSession綁定的,每一個事物都是單獨的SqlSession。
  • Executor:Executor是MyBatis的SQL執行器,MyBatis中對數據庫所有的增刪改查操作都是由Executor組件完成的。
  • StatementHandler:StatementHandler封裝了對JDBCStatement對象的操作,比如爲Statement對象設置參數,調用Statement接口提供的方法與數據庫交互,等等。
  • ParameterHandler:當MyBatis框架使用的Statement類型爲CallableStatement和PreparedStatement時,ParameterHandler用於爲Statement對象參數佔位符設置值。
  • ResultSetHandler:ResultSetHandler封裝了對JDBC中的ResultSet對象操作,當執行SQL類型爲SELECT語句時,ResultSetHandler用於將查詢結果轉換成Java對象。
  • TypeHandler:TypeHandler是MyBatis中的類型處理器,用於處理Java類型與JDBC類型之間的映射。它的作用主要體現在能夠根據Java類型調用PreparedStatement或CallableStatement對象對應的setXXX()方法爲Statement對象設置值,而且能夠根據Java類型調用ResultSet對象對應的getXXX()獲取SQL執行結果。

 

問:mybatis dao層只有接口,實現類怎麼來?

**Mapper是一個接口,我們調用SqlSession對象getMapper()返回的到底是什麼呢?

SqlSession執行Mappper過程

1、Mapper接口的註冊:解析XML或者註解

接口方法名一般和MapperXML配置文件中<select|update|delete|insert>標籤的id屬性相同

final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);

 

實現過程:MyBatis中通過MapperProxy類實現動態代理,invoke()方法中爲通用的攔截邏輯。使用MapperProxyFactory創建Mapper動態代理對象。MapperProxyFactory類的工廠方法newInstance()是非靜態的。也就是說,使用MapperProxyFactory創建Mapper動態代理對象首先需要創建MapperProxyFactory實例,而MapperProxyFactory在啓動時會創建加載到Configuration對象。MyBatis通過mapperRegistry屬性註冊Mapper接口與MapperProxyFactory對象之間的對應關係。

整體流程:

MyBatis中Mapper的配置分爲兩部分,分別爲Mapper接口和MapperSQL配置。MyBatis通過動態代理的方式創建Mapper接口的代理對象,MapperProxy類中定義了Mapper方法執行時的攔截邏輯,通過MapperProxyFactory創建代理實例,MyBatis啓動時,會將MapperProxyFactory註冊到Configuration對象中。另外,MyBatis通過MappedStatement類描述Mapper SQL配置信息,框架啓動時,會解析Mapper SQL配置,將所有的MappedStatement對象註冊到Configuration對象中。通過Mapper代理對象調用Mapper接口中定義的方法時,會執行MapperProxy類中的攔截邏輯,將Mapper方法的調用轉換爲調用SqlSession提供的API方法。在SqlSession的API方法中通過Mapper的Id找到對應的MappedStatement對象,獲取對應的SQL信息,通過StatementHandler操作JDBC的Statement對象完成與數據庫的交互,然後通過ResultSetHandler處理結果集,將結果返回給調用者。

 

jdk動態代理和mybatis代理的區分:

jdk的動態代理:在實現InvocationHandler的代理類裏面,需要傳入一個被代理對象的實現類。

mybatis動態代理:MyBatis中通過MapperProxy類實現動態代理,只需要接口類型+方法的名稱就可找到StatementID,所有不需要實現類。

 

緩存模塊

緩存模塊主要是在cache包下面

MyBatis通過Cache接口定義緩存對象的行爲

 

MyBatis的緩存分爲一級緩存和二級緩存,如果使用不當,則可能會帶來數據一致性問題,下面介紹下一級緩存和二級緩存。

一級緩存

一級緩存默認是開啓的,而且不能關閉。至於一級緩存爲什麼不能關閉,MyBatis核心開發人員做出瞭解釋:MyBatis的一些關鍵特性(例如通過<association>和<collection>建立級聯映射、避免循環引用(circular references)、加速重複嵌套查詢等)都是基於MyBatis一級緩存實現的,而且MyBatis結果集映射相關代碼重度依賴CacheKey,所以目前MyBatis不支持關閉一級緩存。

以下是官方原話:https://mybatis.org/mybatis-3/zh/java-api.html#sqlSessions

默認情況下,本地緩存數據的生命週期等同於整個 session 的週期。由於緩存會被用來解決循環引用問題和加快重複嵌套查詢的速度,所以無法將其完全禁用。但是你可以通過設置 localCacheScope=STATEMENT 來只在語句執行時使用緩存。

注意,如果 localCacheScope 被設置爲 SESSION,對於某個對象,MyBatis 將返回在本地緩存中唯一對象的引用。對返回的對象(例如 list)做出的任何修改將會影響本地緩存的內容,進而將會影響到在本次 session 中從緩存返回的值。因此,不要對 MyBatis 所返回的對象作出更改(這句話仍然不太理解),以防後患。

問:爲什麼mybatis一級緩存不能關閉?

源碼角度來看:

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 {
      // 若緩存中獲取不到,則調用queryFromDataBase方法從數據庫中查詢
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }

從代碼中看,查詢時,沒有任何的前置條件,默認走緩存中拿,取不到再從數據庫中查詢。注意查詢時會判斷ms.isFlushCacheRequired()是否清空緩存。因此可以配置flushCacheRequired可以在查詢時,清空當前緩存。當然不建議這麼操作!如下操作:將flushCache設置爲true。

<select ... flushCache="false" useCache="true|false"/>

一級緩存作用域的控制:

MyBatis提供了一個配置參數localCacheScope,用於控制一級緩存的級別,該參數的取值爲SESSION、STATEMENT,當指定localCacheScope參數值爲SESSION時,緩存對整個SqlSession有效,只有執行DML語句(更新語句)時,緩存纔會被清除。當localCacheScope值爲STATEMENT時,緩存僅對當前執行的語句有效,當語句執行完畢後,緩存就會被清空。

MyBatis的一級緩存默認是SqlSession級別的緩存,在介紹MyBatis核心組件時,有提到過SqlSession提供了面向用戶的API,但是真正執行SQL操作的是Executor組件。Executor採用模板方法設計模式,BaseExecutor類用於處理一些通用的邏輯,其中一級緩存相關的邏輯就是在BaseExecutor類中完成的。

一級緩存使用PerpetualCache實例實現,在BaseExecutor類中維護了兩個PerpetualCache屬性。其中,localCache屬性用於緩存MyBatis查詢結果,localOutputParameterCache屬性用於緩存存儲過程調用結果。

其中緩存key的生成:BaseExecutor類的createCacheKey()方法。

緩存的Key與下面這些因素有關:

(1)Mapper的Id,即Mapper命名空間與<select|update|insert|delete>標籤的Id組成的全侷限定名。

(2)查詢結果的偏移量及查詢的條數。

(3)具體的SQL語句及SQL語句中需要傳遞的所有參數。

(4)MyBatis主配置文件中,通過<environment>標籤配置的環境信息對應的Id屬性值。

 

二級緩存

默認開啓,也就是說 在MyBatis主配置文件中指定cacheEnabled屬性值爲true 這個是可以不需要配置的。只有當<cache>標籤配置了,二級緩存才能生效,只有配置了這個纔會真正的使用二級緩存。

在配置Mapper文件中,通過useCache屬性指定Mapper執行時是否使用緩存,這個也是默認開啓的。

2.1 二級緩存的使用

官網描述:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html#cache

默認情況下,只啓用了本地的會話緩存,它僅僅對一個會話中的數據進行緩存。 要啓用全局的二級緩存,只需要在你的 SQL 映射文件中添加一行:

<cache/>

基本上就是這樣。這個簡單語句的效果如下:

映射語句文件中的所有 select 語句的結果將會被緩存。

映射語句文件中的所有 insert、update 和 delete 語句會刷新緩存。

緩存會使用最近最少使用算法(LRU, Least Recently Used)算法來清除不需要的緩存。

緩存不會定時進行刷新(也就是說,沒有刷新間隔)。

緩存會保存列表或對象(無論查詢方法返回哪種)的 1024 個引用。

緩存會被視爲讀/寫緩存,這意味着獲取到的對象並不是共享的,可以安全地被調用者修改,而不干擾其他調用者或線程所做的潛在修改。

提示 緩存只作用於 cache 標籤所在的映射文件中的語句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的語句將不會被默認緩存。你需要使用 @CacheNamespaceRef 註解指定緩存作用域。

這些屬性可以通過 cache 元素的屬性來修改。比如:

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

這個更高級的配置創建了一個 FIFO 緩存,每隔 60 秒刷新,最多可以存儲結果對象或列表的 512 個引用,而且返回的對象被認爲是隻讀的,因此對它們進行修改可能會在不同線程中的調用者產生衝突。

可用的清除策略有:

LRU – 最近最少使用:移除最長時間不被使用的對象。

FIFO – 先進先出:按對象進入緩存的順序來移除它們。

SOFT – 軟引用:基於垃圾回收器狀態和軟引用規則移除對象。

WEAK – 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除對象。

默認的清除策略是 LRU。

flushInterval(刷新間隔)屬性可以被設置爲任意的正整數,設置的值應該是一個以毫秒爲單位的合理時間量。 默認情況是不設置,也就是沒有刷新間隔,緩存僅僅會在調用語句時刷新。

size(引用數目)屬性可以被設置爲任意正整數,要注意欲緩存對象的大小和運行環境中可用的內存資源。默認值是 1024。

readOnly(只讀)屬性可以被設置爲 true 或 false。只讀的緩存會給所有調用者返回緩存對象的相同實例。 因此這些對象不能被修改。這就提供了可觀的性能提升。而可讀寫的緩存會(通過序列化)返回緩存對象的拷貝。 速度上會慢一些,但是更安全,因此默認值是 false。

提示 二級緩存是事務性的。這意味着,當 SqlSession 完成並提交時,或是完成並回滾,但沒有執行 flushCache=true 的 insert/delete/update 語句時,緩存會獲得更新。

 

大致原理:mybatis用了一個裝飾器類CachingExecutor。如果啓用了二級緩存(默認開啓),mybatis在創建Executor進行裝飾。只要 cacheEnabled=true 基本執行器就會被裝飾。有沒有配置<cache/>標籤,決定了在啓動的時候會不會創建這個 mapper 的 Cache 對象,最終會影響到 CachingExecutor query 方法裏面的判斷,如下圖所示:

 

 

2.2 二級緩存源碼分析

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  //是否配置了二級緩存,如果二級緩存配置不爲空,則走二級緩存查詢
  Cache cache = ms.getCache();
  if (cache != null) {
    //判斷是否開啓需要刷新緩存
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      //從二級緩存中查詢是否存在key
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        //此時put提交,需要在sqlSession.close,纔會commit
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

 

我們可以在單個statement id上顯形關閉二級緩存(默認userCache是true):

<select id="getUser" resultType="org.entity.User" useCache="false">

二級緩存的全局關閉:

官網描述:

cacheEnabled | 全局性地開啓或關閉所有映射器配置文件中已配置的任何緩存

鏈接地址:https://mybatis.org/mybatis-3/zh/configuration.html#settings

 

2.3 二級緩存回收策略

 1、LRU:最近最少使用的策略,移除最長時間不被使用的對象。

 2、FIFO:先進先出策略,按對象進入緩存的順序來移除它們。

 3、SOFT:軟引用策略,移除基於垃圾回收器狀態和軟引用規則的對象。

 4、WEAK:弱引用策略,更積極地移除基於垃圾收集器狀態和弱引用規則的對象。

 

 注:軟引用與弱引用的區別:

  (1)軟引用: 軟引用是用來描述一些有用但並不是必需的對象, 對於軟引用關聯着的對象,只有在內存不足的時候JVM纔會回收該對象

  (2)弱引用: 弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象

 

聯想一下:redis-緩存失效三種策略(FIFO 、LRU、LFU)

當緩存需要被清理時(比如空間佔用已經接近臨界值了),需要使用某種淘汰算法來決定清理掉哪些數據。常用的淘汰算法有下面幾種:

FIFO:First In First Out,先進先出。判斷被存儲的時間,離目前最遠的數據優先被淘汰。

LRU:Least Recently Used,最近最少使用。判斷最近被使用的時間,目前最遠的數據優先被淘汰。

LFU:Least Frequently Used,最不經常使用。在一段時間內,數據被使用次數最少的,優先被淘汰。

 

在實際使用過程中,我們使用mybatis二級緩存的場景比較少,可以做一個大致的瞭解。

 

mybatis源碼分析:

1、通過SqlSessionFactoryBuilder 使用XMLConfigBuilder解析配置文件

2、將解析的數據庫放入Configuration對象中

3、通過Configuration獲取到DefaultSqlSessionFactory(屬性裏面包含Executor)

4、再獲取SqlSession(Executor底層封裝增刪改查的方法,自帶一級緩存,底層HashMap)

5、創建session實例.openSession()

5.1 創建事務管理器

5.2、默認創建SimpleExecutor執行器 ,將簡單執行器丟給緩存執行器CachingExecutor(通過 構造函數傳遞。由於默認開啓了二級緩存,如果沒有二級緩存配置,仍然執行的SimpleExecutor)最後丟給DefaultSqlSession。

6、通過代理設計模式獲取到MapperProxy,執行目標方法(JDK動態代理)

 

整體流程如下圖所示:

 

 

總結:

一級緩存是在Executor中實現的。MyBatis的Executor組件有3種不同的實現,分別爲SimpleExecutor、ReuseExecutor和BatchExecutor。這些類都繼承自BaseExecutor,在BaseExecutor類的query()方法中,首先從緩存中獲取查詢結果,如果獲取不到,則從數據庫中查詢結果,然後將查詢結果緩存起來。而MyBatis的二級緩存則是通過裝飾器模式實現的,當配置cache標籤開啓了二級緩存,MyBatis框架會使用CachingExecutor對SimpleExecutor、ReuseExecutor或者BatchExecutor進行裝飾,當執行查詢操作時,對查詢結果進行緩存,執行更新操作時則更新二級緩存。

 

思考:

對於mybatis緩存實現這一塊,首先會使用CachingExecutor對SimpleExecutor、ReuseExecutor或者BatchExecutor進行裝飾。

按照我們正常邏輯實現,對於數據的查詢,首先查詢一級緩存,如果沒有,查詢二級緩存,二級緩存沒有去查詢數據庫,最後將查詢結果緩存到一二級緩存中。

但是在mybatis中的實現,對於一級緩存的操作,不設置開關,直接去查詢一級緩存。有些爲了要解決一級緩存的問題,mybatis提供了flushCache會判斷查詢之前是否需要清除緩存,然後在查詢結束時判斷緩存作用域如果是statement則清空一級緩存。對於設置爲statement級別的來說,實現起來就有點冗餘。(當然只是筆者自己觀點,可能是見識淺薄)。

當然,mybatis和spring結合起來使用時,一個事物綁定一個sqlsession。相當於mybatis一級緩存就是相當於存在ThreadLocal的數據一樣,只在當前事物有效,在分佈式環境中當前一些簡單事物存在幾十毫秒的緩存髒讀也是能接受的。

 

通過文章的解讀,我們要避免多個線程操作同一個session。

 

文章的最後,對於官方提及的一級緩存用於解決循環引用問題和加快重複嵌套查詢的速度,這部分有興趣的同學可以看看代碼,具體在什麼場景以及使用到解決這部分問題的代碼位置。

 

相關書籍:

MyBatis技術內幕、MyBatis 3源碼深度解析

MyBatis官網鏈接:https://mybatis.org/mybatis-3/zh/index.html

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章