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
緩存是通常的ORM 框架都會提供的功能,目的就是提高查詢的效率和減小數據庫的壓力。跟Hibernate 同樣,MyBatis 也有一級緩存和二級緩存,而且預留了集成第三方緩存的接口。算法
緩存體系結構:sql
MyBatis 跟緩存相關的類都在cache 包裏面,其中有一個Cache 接口,只有一個默認的實現類 PerpetualCache,它是用HashMap 實現的。咱們能夠經過 如下類找到這個緩存的廬山真面目數據庫
DefaultSqlSessionapache
BaseExecutor緩存
PerpetualCache localCache安全
private Map<Object, Object> cache = new HashMap();session
除此以外,還有不少的裝飾器,經過這些裝飾器能夠額外實現不少的功能:回收策略、日誌記錄、定時刷新等等。可是不管怎麼裝飾,通過多少層裝飾,最後使用的仍是基本的實現類(默認PerpetualCache)。能夠經過 CachingExecutor 類 Debug 去查看。
全部的緩存實現類整體上可分爲三類:基本緩存、淘汰算法緩存、裝飾器緩存。
一級緩存也叫本地緩存,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),對於每一次查詢,都會嘗試根據查詢的條件去本地緩存中查找是否在緩存中,若是在緩存中,就直接從緩存中取出,而後返回給用戶;不然,從數據庫讀取數據,將查詢結果存入緩存並返回給用戶。
一級緩存的生命週期有多長?
SqlSession 一級緩存的工做流程:
對於某個查詢,根據statementId,params,rowBounds來構建一個key值,根據這個key值去緩存Cache中取出對應的key值存儲的緩存結果
判斷從Cache中根據特定的key值取的數據數據是否爲空,便是否命中;
若是命中,則直接將緩存結果返回;
若是沒命中:
接下來咱們來驗證一下,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),會發現咱們兩次的查詢就發送了一次查詢數據庫的操做,這說明了緩存在發生做用:
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)沒法關閉,可是有兩種級別可選:
二級緩存是用來解決一級緩存不能跨會話共享的問題的,範圍是namespace 級別的,能夠被多個SqlSession 共享(只要是同一個接口裏面的相同方法,均可以共享),生命週期和應用同步。若是你的MyBatis使用了二級緩存,而且你的Mapper和select語句也配置使用了二級緩存,那麼在執行select查詢的時候,MyBatis會先從二級緩存中取輸入,其次纔是一級緩存,即MyBatis查詢數據的順序是:二級緩存 —> 一級緩存 —> 數據庫。
做爲一個做用範圍更廣的緩存,它確定是在SqlSession 的外層,不然不可能被多個SqlSession 共享。而一級緩存是在SqlSession 內部的,因此第一個問題,確定是工做在一級緩存以前,也就是隻有取不到二級緩存的狀況下才到一個會話中去取一級緩存。第二個問題,二級緩存放在哪一個對象中維護呢? 要跨會話共享的話,SqlSession 自己和它裏面的BaseExecutor 已經知足不了需求了,那咱們應該在BaseExecutor 以外建立一個對象。
實際上MyBatis 用了一個裝飾器的類來維護,就是CachingExecutor。若是啓用了二級緩存,MyBatis 在建立Executor 對象的時候會對Executor 進行裝飾。CachingExecutor 對於查詢請求,會判斷二級緩存是否有緩存結果,若是有就直接返回,若是沒有委派交給真正的查詢器Executor 實現類,好比SimpleExecutor 來執行查詢,再走到一級緩存的流程。最後會把結果緩存起來,而且返回給用戶。
開啓二級緩存的方法
第一步:配置 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"/> 複製代碼
基本上就是這樣。這個簡單語句的效果以下:
這個更高級的配置建立了一個 FIFO 緩存,每隔 60 秒刷新,最多能夠存儲結果對象或列表的 512 個引用,並且返回的對象被認爲是隻讀的,所以對它們進行修改可能會在不一樣線程中的調用者產生衝突。可用的清除策略有:
默認的清除策略是 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。
何時開啓二級緩存?
一級緩存默認是打開的,二級緩存須要配置才能夠開啓。那麼咱們必須思考一個問題,在什麼狀況下才有必要去開啓二級緩存?
若是要讓多個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 語句執行時不要刷新緩存。