前言
在計算機的世界中,緩存無處不在,操做系統有操做系統的緩存,數據庫也會有數據庫的緩存,各類中間件如Redis也是用來充當緩存的做用,編程語言中又能夠利用內存來做爲緩存。天然的,做爲一款優秀的ORM框架,MyBatis中又豈能少得了緩存,那麼本文的目的就是帶領你們一塊兒探究一下MyBatis的緩存是如何實現的,只需給我五分鐘,帶你完全掌握MyBatis的緩存工做原理。java
爲何要緩存
在計算機的世界中,CPU的處理速度可謂是身先士卒,遠遠甩開了其餘操做,尤爲是I/O操做,除了那種CPU密集型的系統,其他大部分的業務系統性能瓶頸最後或多或少都會出如今I/O操做上,因此爲了減小磁盤的I/O次數,那麼緩存是必不可少的,經過緩存的使用咱們能夠大大減小I/O操做次數,從而在必定程度上彌補了I/O操做和CPU處理速度之間的鴻溝。而在咱們ORM框架中引入緩存的目的就是爲了減小讀取數據庫的次數,從而提高查詢的效率。程序員
MyBatis緩存
MyBatis中的緩存相關類都在cache包下面,並且定義了一個頂級接口Cache,默認只有一個實現類PerpetualCache,PerpetualCache中是內部維護了一個HashMap來實現緩存。面試
下圖就是MyBatis中緩存相關類:redis
須要注意的是decorators包下面的全部類也實現了Cache接口,那麼爲何我仍是要說Cache只有一個實現類呢?其實看名字就知道了,這個包裏面所有是裝飾器,也就是說這實際上是裝飾器模式的一種實現。算法
咱們隨意打開一個裝飾器:sql
能夠看到,最終都是調用了delegate來實現,只是將部分功能作了加強,其自己都須要依賴Cache的惟一實現類PerpetualCache(由於裝飾器內須要傳入Cache對象,故而只能傳入PerpetualCache對象,由於接口是沒法直接new出來傳進去的)。數據庫
在MyBatis中存在兩種緩存,即一級緩存和二級緩存。apache
一級緩存
一級緩存也叫本地緩存,在MyBatis中,一級緩存是在會話(SqlSession)層面實現的,這就說明一級緩存做用範圍只能在同一個SqlSession中,跨SqlSession是無效的。編程
MyBatis中一級緩存是默認開啓的,不須要任何配置。咱們先來看一個例子驗證一下一級緩存是否是真的存在,做用範圍又是否是真的只是對同一個SqlSession有效。後端
一級緩存真的存在嗎
package com.lonelyWolf.mybatis; import com.lonelyWolf.mybatis.mapper.UserAddressMapper; import com.lonelyWolf.mybatis.mapper.UserMapper; import com.lonelyWolf.mybatis.model.LwUser; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; import java.util.List; public class TestMyBatisCache { public static void main(String[] args) throws IOException { String resource = "mybatis-config.xml"; //讀取mybatis-config配置文件 InputStream inputStream = Resources.getResourceAsStream(resource); //建立SqlSessionFactory對象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //建立SqlSession對象 SqlSession session = sqlSessionFactory.openSession(); UserMapper userMapper = session.getMapper(UserMapper.class); List<LwUser> userList = userMapper.selectUserAndJob(); List<LwUser> userList2 = userMapper.selectUserAndJob(); } }
執行後,輸出結果以下:
咱們能夠看到,sql語句只打印了一次,這就說明第2次用到了緩存,這也足以證實一級緩存確實是存在的並且默認就是是開啓的。
一級緩存做用範圍
如今咱們再來驗證一下一級緩存是否真的只對同一個SqlSession有效,咱們對上面的示例代碼進行以下改變:
SqlSession session1 = sqlSessionFactory.openSession(); SqlSession session2 = sqlSessionFactory.openSession(); UserMapper userMapper1 = session1.getMapper(UserMapper.class); UserMapper userMapper2 = session2.getMapper(UserMapper.class); List<LwUser> userList = userMapper1.selectUserAndJob(); List<LwUser> userList2 = userMapper2.selectUserAndJob();
這時候再次運行,輸出結果以下:
能夠看到,打印了2次,沒有用到緩存,也就是不一樣SqlSession中不能共享一級緩存。
一級緩存原理分析
首先讓咱們來想想,既然一級緩存的做用域只對同一個SqlSession有效,那麼一級緩存應該存儲在哪裏比較合適是呢?
是的,天然是存儲在SqlSession內是最合適的,那咱們來看看SqlSession的惟一實現類DefaultSqlSession:
DefaultSqlSession中只有5個成員屬性,後面3個不用說,確定不可能用來存儲緩存,而後Configuration又是一個全局的配置文件,也不合適存儲一級緩存,這麼看來就只有Executor比較合適了,由於咱們知道,SqlSession只提供對外接口,實際執行sql的就是Executor。
既然這樣,那咱們就進去看看Executor的實現類BaseExecutor:
看到果真有一個localCache。而上面咱們有提到PerpetualCache內緩存是用一個HashMap來存儲緩存的,那麼接下來你們確定就有如下問題:
-
緩存是何時建立的?
-
緩存的key是怎麼定義的?
-
緩存在什麼時候使用
-
緩存在何時會失效?
接下來就讓咱們逐一分析
一、一級緩存CacheKey的構成
既然緩存那麼確定是針對的查詢語句,一級緩存的建立就是在BaseExecutor中的query方法內建立的:
createCacheKey這個方法的代碼就不貼了,在這裏我總結了一下CacheKey的組成,CacheKey主要是由如下6部分組成
-
一、將Statement中的id添加到CacheKey對象中的updateList屬性
-
二、將offset(分頁偏移量)添加到CacheKey對象中的updateList屬性(若是沒有分頁則默認0)
-
三、將limit(每頁顯示的條數)添加到CacheKey對象中的updateList屬性(若是沒有分頁則默認Integer.MAX_VALUE)
-
四、將sql語句(包括佔位符?)添加到CacheKey對象中的updateList屬性
-
五、循環用戶傳入的參數,並將每一個參數添加到CacheKey對象中的updateList屬性
-
六、若是有配置Environment,則將Environment中的id添加到CacheKey對象中的updateList屬性
二、一級緩存的使用
建立完CacheKey以後,咱們繼續進入query方法:
能夠看到,在查詢以前就會去localCache中根據CacheKey對象來獲取緩存,獲取不到纔會調用後面的queryFromDatabase方法
三、一級緩存的建立
queryFromDatabase方法中會將查詢獲得的結果存儲到localCache中
四、一級緩存何時會被清除
一級緩存的清除主要有如下兩個地方:
-
一、就是獲取緩存以前會先進行判斷用戶是否配置了flushCache=true屬性(參考一級緩存的建立代碼截圖),若是配置了則會清除一級緩存。
-
二、MyBatis全局配置屬性localCacheScope配置爲Statement時,那麼完成一次查詢就會清除緩存。
-
三、在執行commit,rollback,update方法時會清空一級緩存。
PS:利用插件咱們也能夠本身去將緩存清除,後面咱們會介紹插件相關知識。
二級緩存
一級緩存由於只能在同一個SqlSession中共享,因此會存在一個問題,在分佈式或者多線程的環境下,不一樣會話之間對於相同的數據可能會產生不一樣的結果,由於跨會話修改了數據是不能互相感知的,因此就有可能存在髒數據的問題,正由於一級緩存存在這種不足,因此咱們須要一種做用域更大的緩存,這就是二級緩存。
二級緩存的做用範圍
一級緩存做用域是SqlSession級別,因此它存儲的SqlSession中的BaseExecutor之中,可是二級緩存目的就是要實現做用範圍更廣,那確定是要實現跨會話共享的,在MyBatis中二級緩存的做用域是namespace,也就是做用範圍是同一個命名空間,因此很顯然二級緩存是須要存儲在SqlSession以外的,那麼二級緩存應該存儲在哪裏合適呢?
在MyBatis中爲了實現二級緩存,專門用了一個裝飾器來維護,這就是咱們上一篇文章介紹Executor時還留下的沒有介紹的一個對象:CachingExecutor。
如何開啓二級緩存
二級緩存相關的配置有三個地方:一、mybatis-config中有一個全局配置屬性,這個不配置也行,由於默認就是true。
<setting name="cacheEnabled" value="true"/>
想詳細瞭解mybatis-config的能夠點擊這裏。二、在Mapper映射文件內須要配置緩存標籤:
<cache/> 或 <cache-ref namespace="com.lonelyWolf.mybatis.mapper.UserAddressMapper"/>
想詳細瞭解Mapper映射的全部標籤屬性配置能夠點擊這裏。三、在select查詢語句標籤上配置useCache屬性,以下:
<select id="selectUserAndJob" resultMap="JobResultMap2" useCache="true"> select * from lw_user </select>
以上配置第1點是默認開啓的,也就是說咱們只要配置第2點就能夠打開二級緩存了,而第3點是當咱們須要針對某一條語句來配置二級緩存時候則可使用。
不過開啓二級緩存的時候有兩點須要注意:一、須要commit事務以後纔會生效 二、若是使用的是默認緩存,那麼結果集對象須要實現序列化接口(Serializable)
若是不實現序列化接口則會報以下錯誤:
接下來咱們經過一個例子來驗證一下二級緩存的存在,仍是用上面一級緩存的例子進行以下改造:
SqlSession session1 = sqlSessionFactory.openSession(); UserMapper userMapper1 = session1.getMapper(UserMapper.class); List<LwUser> userList = userMapper1.selectUserAndJob(); session1.commit();//注意這裏須要commit,不然緩存不會生效 SqlSession session2 = sqlSessionFactory.openSession(); UserMapper userMapper2 = session2.getMapper(UserMapper.class); List<LwUser> userList2 = userMapper2.selectUserAndJob();
而後UserMapper.xml映射文件中,新增以下配置:
<cache/>
運行代碼,輸出以下結果:
上面輸出結果中只輸出了一次sql,說明用到了緩存,而由於咱們是跨會話的,因此確定就是二級緩存生效了。
二級緩存原理分析
上面咱們提到二級緩存是經過CachingExecutor對象來實現的,那麼就讓咱們先來看看這個對象:
咱們看到CachingExecutor中只有2個屬性,第1個屬性不用說了,由於CachingExecutor自己就是Executor的包裝器,因此屬性TransactionalCacheManager確定就是用來管理二級緩存的,咱們再進去看看TransactionalCacheManager對象是如何管理緩存的:
TransactionalCacheManager內部很是簡單,也是維護了一個HashMap來存儲緩存。HashMap中的value是一個TransactionalCache對象,繼承了Cache。
注意上面有一個屬性是臨時存儲二級緩存的,爲何要有這個屬性,咱們下面會解釋。
一、二級緩存的建立和使用
咱們在讀取mybatis-config全局配置文件的時候會根據咱們配置的Executor類型來建立對應的三種Executor中的一種,而後若是咱們開啓了二級緩存以後,只要開啓(全局配置文件中配置爲true)就會使用CachingExecutor來對咱們的三種基本Executor進行包裝,即便Mapper.xml映射文件沒有開啓也會進行包裝。
接下來咱們看看CachingExecutor中的query方法:
上面方法大體通過以下流程:
-
一、建立一級緩存的CacheKey
-
二、獲取二級緩存
-
三、若是沒有獲取到二級緩存則執行被包裝的Executor對象中的query方法,此時會走一級緩存中的流程。
-
四、查詢到結果以後將結果進行緩存。
須要注意的是在事務提交以前,並不會真正存儲到二級緩存,而是先存儲到一個臨時屬性,等事務提交以後纔會真正存儲到二級緩存。這麼作的目的就是防止髒讀。由於假如你在一個事務中修改了數據,而後去查詢,這時候直接緩存了,那麼假如事務回滾了呢?因此這裏會先臨時存儲一下。因此咱們看一下commit方法:
二級緩存如何進行包裝
最開始咱們提到了一些緩存的包裝類,這些都到底有什麼用呢?在回答這個問題以前,咱們先斷點一下看看獲取到的二級緩存長啥樣:
從上面能夠看到,通過了層層包裝,從內到外一次通過以下包裝:
-
一、PerpetualCache:第一層緩存,這個是緩存的惟一實現類,確定須要。
-
二、LruCache:二級緩存淘汰機制之一。由於咱們配置的默認機制,而默認就是LRU算法淘汰機制。淘汰機制總共有4中,咱們能夠本身進行手動配置。
-
三、SerializedCache:序列化緩存。這就是爲何開啓了默認二級緩存咱們的結果集對象須要實現序列化接口。
-
四、LoggingCache:日誌緩存。
-
五、SynchronizedCache:同步緩存機制。這個是爲了保證多線程機制下的線程安全性。
下面就是MyBatis中全部緩存的包裝彙總:
二級緩存應該開啓嗎
既然一級緩存默認是開啓的,而二級緩存是須要咱們手動開啓的,那麼咱們何時應該開啓二級緩存呢?
一、由於全部的update操做(insert,delete,uptede)都會觸發緩存的刷新,從而致使二級緩存失效,因此二級緩存適合在讀多寫少的場景中開啓。
二、由於二級緩存針對的是同一個namespace,因此建議是在單表操做的Mapper中使用,或者是在相關表的Mapper文件中共享同一個緩存。
自定義緩存
一級緩存可能存在髒讀狀況,那麼二級緩存是否也可能存在呢?
是的,默認的二級緩存畢竟也是存儲在本地緩存,因此對於微服務下是可能出現髒讀的狀況的,因此這時候咱們可能會須要自定義緩存,好比利用redis來存儲緩存,而不是存儲在本地內存當中。
MyBatis官方提供的第三方緩存
MyBatis官方也提供了一些第三方緩存的支持,如:encache和redis。下面咱們以redis爲例來演示一下:引入pom文件:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency>
而後緩存配置以下:
<cache type="org.mybatis.caches.redis.RedisCache"></cache>
而後在默認的resource路徑下新建一個redis.properties文件:
host=localhost port=6379 12
而後執行上面的示例,查看Cache,已經被Redis包裝:
本身實現二級緩存
若是要實現一個本身的緩存的話,那麼咱們只須要新建一個類實現Cache接口就行了,而後重寫其中的方法,以下:
package com.lonelyWolf.mybatis.cache; import org.apache.ibatis.cache.Cache; public class MyCache implements Cache { @Override public String getId() { return null; } @Override public void putObject(Object o, Object o1) { } @Override public Object getObject(Object o) { return null; } @Override public Object removeObject(Object o) { return null; } @Override public void clear() { } @Override public int getSize() { return 0; } }
上面自定義的緩存中,咱們只須要在對應方法,如putObject方法,咱們把緩存存到咱們想存的地方就好了,方法所有重寫以後,而後配置的時候type配上咱們本身的類就能夠實現了,在這裏咱們就不作演示了
總結
本文主要分析了MyBatis的緩存是如何實現的,而且分別演示了一級緩存和二級緩存,並分析了一級緩存和二級緩存所存在的問題,最後也介紹瞭如何使用第三方緩存和如何自定義咱們本身的緩存,經過本文,我想你們應該能夠完全掌握MyBatis的緩存工做原理了。