Mybatis緩存機制詳解

什麼是mybatis?

MyBatis 本是apache的一個開源項目iBatis, 2010年這個項目由apache software foundation 遷移到了google code,而且更名爲MyBatis 。2013年11月遷移到Github。 iBATIS一詞來源於「internet」和「abatis」的組合,是一個基於Java的持久層框架。git

iBATIS提供的持久層框架包括SQL Maps和Data Access Objects(DAOs)github

整理了一些mybatis的學習資料redis

MyBatis 緩存詳解

  緩存是通常的ORM 框架都會提供的功能,目的就是提高查詢的效率和減小數據庫的壓力。跟Hibernate 同樣,MyBatis 也有一級緩存和二級緩存,而且預留了集成第三方緩存的接口。算法

  緩存體系結構:sql

image

  MyBatis 跟緩存相關的類都在cache 包裏面,其中有一個Cache 接口,只有一個默認的實現類 PerpetualCache,它是用HashMap 實現的。咱們能夠經過 如下類找到這個緩存的廬山真面目數據庫

DefaultSqlSessionapache

BaseExecutor緩存

PerpetualCache localCache安全

private Map<Object, Object> cache = new HashMap();session

  除此以外,還有不少的裝飾器,經過這些裝飾器能夠額外實現不少的功能:回收策略、日誌記錄、定時刷新等等。可是不管怎麼裝飾,通過多少層裝飾,最後使用的仍是基本的實現類(默認PerpetualCache)。能夠經過 CachingExecutor 類 Debug 去查看。

image

  全部的緩存實現類整體上可分爲三類:基本緩存、淘汰算法緩存、裝飾器緩存。

image

一級緩存(本地緩存):

  一級緩存也叫本地緩存,MyBatis 的一級緩存是在會話(SqlSession)層面進行緩存的。MyBatis 的一級緩存是默認開啓的,不須要任何的配置。首先咱們必須去弄清楚一個問題,在MyBatis 執行的流程裏面,涉及到這麼多的對象,那麼緩存PerpetualCache 應該放在哪一個對象裏面去維護?若是要在同一個會話裏面共享一級緩存,這個對象確定是在SqlSession 裏面建立的,做爲SqlSession 的一個屬性。

  DefaultSqlSession 裏面只有兩個屬性,Configuration 是全局的,因此緩存只可能放在Executor 裏面維護——SimpleExecutor/ReuseExecutor/BatchExecutor 的父類BaseExecutor 的構造函數中持有了PerpetualCache。在同一個會話裏面,屢次執行相同的SQL 語句,會直接從內存取到緩存的結果,不會再發送SQL 到數據庫。可是不一樣的會話裏面,即便執行的SQL 如出一轍(經過一個Mapper 的同一個方法的相同參數調用),也不能使用到一級緩存。

  每當咱們使用MyBatis開啓一次和數據庫的會話,MyBatis會建立出一個SqlSession對象表示一次數據庫會話。

  在對數據庫的一次會話中,咱們有可能會反覆地執行徹底相同的查詢語句,若是不採起一些措施的話,每一次查詢都會查詢一次數據庫,而咱們在極短的時間內作了徹底相同的查詢,那麼它們的結果極有可能徹底相同,因爲查詢一次數據庫的代價很大,這有可能形成很大的資源浪費。

  爲了解決這一問題,減小資源的浪費,MyBatis會在表示會話的SqlSession對象中創建一個簡單的緩存,將每次查詢到的結果結果緩存起來,當下次查詢的時候,若是判斷先前有個徹底同樣的查詢,會直接從緩存中直接將結果取出,返回給用戶,不須要再進行一次數據庫查詢了。

  以下圖所示,MyBatis會在一次會話的表示----一個SqlSession對象中建立一個本地緩存(local cache),對於每一次查詢,都會嘗試根據查詢的條件去本地緩存中查找是否在緩存中,若是在緩存中,就直接從緩存中取出,而後返回給用戶;不然,從數據庫讀取數據,將查詢結果存入緩存並返回給用戶。

image

一級緩存的生命週期有多長?

  1. MyBatis在開啓一個數據庫會話時,會 建立一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象,Executor對象中持有一個新的PerpetualCache對象;當會話結束時,SqlSession對象及其內部的Executor對象還有PerpetualCache對象也一併釋放掉。
  2. 若是SqlSession調用了close()方法,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用;
  3. 若是SqlSession調用了clearCache(),會清空PerpetualCache對象中的數據,可是該對象仍可以使用;
  4. SqlSession中執行了任何一個update操做(update()、delete()、insert()) ,都會清空PerpetualCache對象的數據,可是該對象能夠繼續使用;

SqlSession 一級緩存的工做流程:

  1. 對於某個查詢,根據statementId,params,rowBounds來構建一個key值,根據這個key值去緩存Cache中取出對應的key值存儲的緩存結果​

  2. 判斷從Cache中根據特定的key值取的數據數據是否爲空,便是否命中;​

  3. 若是命中,則直接將緩存結果返回;​

  4. 若是沒命中:

    1. 去數據庫中查詢數據,獲得查詢結果;
    2. 將key和查詢到的結果分別做爲key,value對存儲到Cache中;
    3. 將查詢結果返回;

  接下來咱們來驗證一下,MyBatis 的一級緩存究竟是不是隻能在一個會話裏面共享,以及跨會話(不一樣session)操做相同的數據會產生什麼問題。判斷是否命中緩存:若是再次發送SQL 到數據庫執行,說明沒有命中緩存;若是直接打印對象,說明是從內存緩存中取到告終果。

一、在同一個session 中共享(不一樣session 不能共享)

//同Session
SqlSession session1 = sqlSessionFactory.openSession();
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1002));
System.out.println(mapper1.selectBlogById(1002));
複製代碼

  執行以上sql咱們能夠看到控制檯打印以下信息(需配置mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl),會發現咱們兩次的查詢就發送了一次查詢數據庫的操做,這說明了緩存在發生做用:

image

  PS:一級緩存在BaseExecutor 的query()——queryFromDatabase()中存入。在queryFromDatabase()以前會get()。

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());
    。。。。。。try {
                ++this.queryStack;//從緩存中獲取
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                if (list != null) {
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {//緩存中獲取不到,查詢數據庫
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
    。。。。。。
    }
複製代碼

2.同一個會話中,update(包括delete)會致使一級緩存被清空

//同Session
SqlSession session1 = sqlSessionFactory.openSession();
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1002));
Blog blog3 = new Blog();
blog3.setBid(1002);
blog3.setName("mybatis緩存機制修改");
mapper1.updateBlog(blog3);
session1.commit();// 注意要提交事務,不然不會清除緩存
System.out.println(mapper1.selectBlogById(1002));
複製代碼

  一級緩存是在BaseExecutor 中的update()方法中調用clearLocalCache()清空的(無條件),query 中會判斷。

public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {        //清除本地緩存
            this.clearLocalCache();
            return this.doUpdate(ms, parameter);
        }
}
複製代碼

3.其餘會話更新了數據,致使讀取到髒數據(一級緩存不能跨會話共享)

SqlSession session1 = sqlSessionFactory.openSession();
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
SqlSession session2 = sqlSessionFactory.openSession();
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1002));
Blog blog3 = new Blog();
blog3.setBid(1002);
blog3.setName("mybatis緩存機制1");
mapper1.updateBlog(blog3);
session1.commit();
System.out.println(mapper2.selectBlogById(1002));
複製代碼

一級緩存的不足:

  使用一級緩存的時候,由於緩存不能跨會話共享,不一樣的會話之間對於相同的數據可能有不同的緩存。在有多個會話或者分佈式環境下,會存在髒數據的問題。若是要解決這個問題,就要用到二級緩存。MyBatis 一級緩存(MyBaits 稱其爲 Local Cache)沒法關閉,可是有兩種級別可選:

  1. session 級別的緩存,在同一個 sqlSession 內,對一樣的查詢將再也不查詢數據庫,直接從緩存中。
  2. statement 級別的緩存,避坑: 爲了不這個問題,能夠將一級緩存的級別設爲 statement 級別的,這樣每次查詢結束都會清掉一級緩存。

二級緩存:

  二級緩存是用來解決一級緩存不能跨會話共享的問題的,範圍是namespace 級別的,能夠被多個SqlSession 共享(只要是同一個接口裏面的相同方法,均可以共享),生命週期和應用同步。若是你的MyBatis使用了二級緩存,而且你的Mapper和select語句也配置使用了二級緩存,那麼在執行select查詢的時候,MyBatis會先從二級緩存中取輸入,其次纔是一級緩存,即MyBatis查詢數據的順序是:二級緩存   —> 一級緩存 —> 數據庫。

  做爲一個做用範圍更廣的緩存,它確定是在SqlSession 的外層,不然不可能被多個SqlSession 共享。而一級緩存是在SqlSession 內部的,因此第一個問題,確定是工做在一級緩存以前,也就是隻有取不到二級緩存的狀況下才到一個會話中去取一級緩存。第二個問題,二級緩存放在哪一個對象中維護呢? 要跨會話共享的話,SqlSession 自己和它裏面的BaseExecutor 已經知足不了需求了,那咱們應該在BaseExecutor 以外建立一個對象。

  實際上MyBatis 用了一個裝飾器的類來維護,就是CachingExecutor。若是啓用了二級緩存,MyBatis 在建立Executor 對象的時候會對Executor 進行裝飾。CachingExecutor 對於查詢請求,會判斷二級緩存是否有緩存結果,若是有就直接返回,若是沒有委派交給真正的查詢器Executor 實現類,好比SimpleExecutor 來執行查詢,再走到一級緩存的流程。最後會把結果緩存起來,而且返回給用戶。

image

  開啓二級緩存的方法

第一步:配置 mybatis.configuration.cache-enabled=true,只要沒有顯式地設置cacheEnabled=false,都會用CachingExecutor 裝飾基本的執行器。

第二步:在Mapper.xml 中配置標籤:

<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
    size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
複製代碼

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

  • 映射語句文件中的全部 select 語句的結果將會被緩存。
  • 映射語句文件中的全部 insert、update 和 delete 語句會刷新緩存。
  • 緩存會使用最近最少使用算法(LRU, Least Recently Used)算法來清除不須要的緩存。
  • 緩存不會定時進行刷新(也就是說,沒有刷新間隔)。
  • 緩存會保存列表或對象(不管查詢方法返回哪一種)的 1024 個引用。
  • 緩存會被視爲讀/寫緩存,這意味着獲取到的對象並非共享的,能夠安全地被調用者修改,而不干擾其餘調用者或線程所作的潛在修改。

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

  • LRU – 最近最少使用:移除最長時間不被使用的對象。
  • FIFO – 先進先出:按對象進入緩存的順序來移除它們。
  • SOFT – 軟引用:基於垃圾回收器狀態和軟引用規則移除對象。
  • WEAK – 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除對象。

默認的清除策略是 LRU。

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

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

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

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

  Mapper.xml 配置了以後,select()會被緩存。update()、delete()、insert()會刷新緩存。:若是cacheEnabled=true,Mapper.xml 沒有配置標籤,還有二級緩存嗎?(沒有)還會出現CachingExecutor 包裝對象嗎?(會)

  只要cacheEnabled=true 基本執行器就會被裝飾。有沒有配置,決定了在啓動的時候會不會建立這個mapper 的Cache 對象,只是最終會影響到CachingExecutorquery 方法裏面的判斷。若是某些查詢方法對數據的實時性要求很高,不須要二級緩存,怎麼辦?咱們能夠在單個Statement ID 上顯式關閉二級緩存(默認是true):

<select id="selectBlog" resultMap="BaseResultMap" useCache="false">
複製代碼

  二級緩存驗證(驗證二級緩存須要先開啓二級緩存)

 一、事務不提交,二級緩存不存在

System.out.println(mapper1.selectBlogById(1002));
// 事務不提交的狀況下,二級緩存不會寫入
// session1.commit();
System.out.println(mapper2.selectBlogById(1002));
複製代碼

  爲何事務不提交,二級緩存不生效?由於二級緩存使用TransactionalCacheManager(TCM)來管理,最後又調用了TransactionalCache 的getObject()、putObject 和commit()方法,TransactionalCache裏面又持有了真正的Cache 對象,好比是通過層層裝飾的PerpetualCache。在putObject 的時候,只是添加到了entriesToAddOnCommit 裏面,只有它的commit()方法被調用的時候纔會調用flushPendingEntries()真正寫入緩存。它就是在DefaultSqlSession 調用commit()的時候被調用的。

二、使用不一樣的session 和mapper,驗證二級緩存能夠跨session 存在取消以上commit()的註釋

三、在其餘的session 中執行增刪改操做,驗證緩存會被刷新

System.out.println(mapper1.selectBlogById(1002));
//主鍵自增返回測試
Blog blog3 = new Blog();
blog3.setBid(1002);
blog3.setName("mybatis緩存機制");
mapper1.updateBlog(blog3);
session1.commit();
System.out.println(mapper2.selectBlogById(1002));
複製代碼

  爲何增刪改操做會清空緩存?在CachingExecutor 的update()方法裏面會調用flushCacheIfRequired(ms),isFlushCacheRequired 就是從標籤裏面渠道的flushCache 的值。而增刪改操做的flushCache 屬性默認爲true。

何時開啓二級緩存?

一級緩存默認是打開的,二級緩存須要配置才能夠開啓。那麼咱們必須思考一個問題,在什麼狀況下才有必要去開啓二級緩存?

  1. 由於全部的增刪改都會刷新二級緩存,致使二級緩存失效,因此適合在查詢爲主的應用中使用,好比歷史交易、歷史訂單的查詢。不然緩存就失去了意義。
  2. 若是多個namespace 中有針對於同一個表的操做,好比Blog 表,若是在一個namespace 中刷新了緩存,另外一個namespace 中沒有刷新,就會出現讀到髒數據的狀況。因此,推薦在一個Mapper 裏面只操做單表的狀況使用。

  若是要讓多個namespace 共享一個二級緩存,應該怎麼作?跨namespace 的緩存共享的問題,可使用來解決:

<cache-ref namespace="com.wuzz.crud.dao.DepartmentMapper" />
複製代碼

  cache-ref 表明引用別的命名空間的Cache 配置,兩個命名空間的操做使用的是同一個Cache。在關聯的表比較少,或者按照業務能夠對錶進行分組的時候可使用。

  注意:在這種狀況下,多個Mapper 的操做都會引發緩存刷新,緩存的意義已經不大了.

第三方緩存作二級緩存

  除了MyBatis 自帶的二級緩存以外,咱們也能夠經過實現Cache 接口來自定義二級緩存。MyBatis 官方提供了一些第三方緩存集成方式,好比ehcache 和redis:github.com/mybatis/red… ,這裏就不過多介紹了。固然,咱們也可使用獨立的緩存服務,不使用MyBatis 自帶的二級緩存。

自定義緩存:

  除了上述自定義緩存的方式,你也能夠經過實現你本身的緩存,或爲其餘第三方緩存方案建立適配器,來徹底覆蓋緩存行爲。

<cache type="com.domain.something.MyCustomCache"/>
複製代碼

  這個示例展現瞭如何使用一個自定義的緩存實現。type 屬性指定的類必須實現 org.mybatis.cache.Cache 接口,且提供一個接受 String 參數做爲 id 的構造器。 這個接口是 MyBatis 框架中許多複雜的接口之一,可是行爲卻很是簡單。

public interface Cache {
  String getId();
  int getSize();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  boolean hasKey(Object key);
  Object removeObject(Object key);
  void clear();
}
複製代碼

  爲了對你的緩存進行配置,只須要簡單地在你的緩存實現中添加公有的 JavaBean 屬性,而後經過 cache 元素傳遞屬性值,例如,下面的例子將在你的緩存實現上調用一個名爲 setCacheFile(String file) 的方法:

<cache type="com.domain.something.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>
複製代碼

  你可使用全部簡單類型做爲 JavaBean 屬性的類型,MyBatis 會進行轉換。 你也可使用佔位符(如 ${cache.file}),以便替換成在配置文件屬性中定義的值。從版本 3.4.2 開始,MyBatis 已經支持在全部屬性設置完畢以後,調用一個初始化方法。 若是想要使用這個特性,請在你的自定義緩存類裏實現 org.apache.ibatis.builder.InitializingObject 接口。

public interface InitializingObject {
  void initialize() throws Exception;
}
複製代碼

  請注意,緩存的配置和緩存實例會被綁定到 SQL 映射文件的命名空間中。 所以,同一命名空間中的全部語句和緩存將經過命名空間綁定在一塊兒。 每條語句能夠自定義與緩存交互的方式,或將它們徹底排除於緩存以外,這能夠經過在每條語句上使用兩個簡單屬性來達成。 默認狀況下,語句會這樣來配置:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
複製代碼

  鑑於這是默認行爲,顯然你永遠不該該以這樣的方式顯式配置一條語句。但若是你想改變默認的行爲,只須要設置 flushCache 和 useCache 屬性。好比,某些狀況下你可能但願特定 select 語句的結果排除於緩存以外,或但願一條 select 語句清空緩存。相似地,你可能但願某些 update 語句執行時不要刷新緩存。

相關文章
相關標籤/搜索