咱們在上一篇文章 ( https://mp.weixin.qq.com/s/4Puee_pPCNArkgnFaYlIjg ) 介紹了 MyBatis 的一級緩存的做用,如何開啓,一級緩存的本質是什麼,一級緩存失效的緣由是什麼? MyBatis 只有一級緩存嗎?來找找答案吧!java
上一篇文章中咱們介紹到了 MyBatis 一級緩存其實就是 SqlSession 級別的緩存,什麼是 SqlSession 級別的緩存呢?一級緩存的本質是什麼呢? 以及一級緩存失效的緣由?我但願你在看下文以前可以回想起來這些內容。sql
MyBatis 一級緩存最大的共享範圍就是一個SqlSession內部,那麼若是多個 SqlSession 須要共享緩存,則須要開啓二級緩存,開啓二級緩存後,會使用 CachingExecutor 裝飾 Executor,進入一級緩存的查詢流程前,先在CachingExecutor 進行二級緩存的查詢,具體的工做流程以下所示數據庫
當二級緩存開啓後,同一個命名空間(namespace) 全部的操做語句,都影響着一個共同的 cache,也就是二級緩存被多個 SqlSession 共享,是一個全局的變量。當開啓緩存後,數據的查詢執行的流程就是 二級緩存 -> 一級緩存 -> 數據庫。緩存
二級緩存默認是不開啓的,須要手動開啓二級緩存,實現二級緩存的時候,MyBatis要求返回的POJO必須是可序列化的。開啓二級緩存的條件也是比較簡單,經過直接在 MyBatis 配置文件中經過安全
<settings> <setting name = "cacheEnabled" value = "true" /> </settings>
來開啓二級緩存,還須要在 Mapper 的xml 配置文件中加入 <cache>
標籤session
設置 cache 標籤的屬性mybatis
cache 標籤有多個屬性,一塊兒來看一些這些屬性分別表明什麼意義app
eviction
: 緩存回收策略,有這幾種回收策略
默認是 LRU 最近最少回收策略框架
flushinterval
緩存刷新間隔,緩存多長時間刷新一次,默認不清空,設置一個毫秒值readOnly
: 是否只讀;true 只讀,MyBatis 認爲全部從緩存中獲取數據的操做都是隻讀操做,不會修改數據。MyBatis 爲了加快獲取數據,直接就會將數據在緩存中的引用交給用戶。不安全,速度快。讀寫(默認):MyBatis 以爲數據可能會被修改size
: 緩存存放多少個元素type
: 指定自定義緩存的全類名(實現Cache 接口便可)blocking
: 若緩存中找不到對應的key,是否會一直blocking,直到有對應的數據進入緩存。咱們繼續以 MyBatis 一級緩存文章中的例子爲基礎,搭建一個知足二級緩存的例子,來對二級緩存進行探究,例子以下(對 一級緩存的例子部分源碼進行修改):ide
Dept.java
//存放在共享緩存中數據進行序列化操做和反序列化操做 //所以數據對應實體類必須實現【序列化接口】 public class Dept implements Serializable { private Integer deptNo; private String dname; private String loc; public Dept() {} public Dept(Integer deptNo, String dname, String loc) { this.deptNo = deptNo; this.dname = dname; this.loc = loc; } get and set... @Override public String toString() { return "Dept{" + "deptNo=" + deptNo + ", dname='" + dname + '\'' + ", loc='" + loc + '\'' + '}'; } }
myBatis-config.xml
在myBatis-config 中添加開啓二級緩存的條件
<!-- 通知 MyBatis 框架開啓二級緩存 --> <settings> <setting name="cacheEnabled" value="true"/> </settings>
DeptDao.xml
還須要在 Mapper 對應的xml中添加 cache 標籤,表示對哪一個mapper 開啓緩存
<!-- 表示DEPT表查詢結果保存到二級緩存(共享緩存) --> <cache/>
對應的二級緩存測試類以下:
public class MyBatisSecondCacheTest { private SqlSession sqlSession; SqlSessionFactory factory; @Before public void start() throws IOException { InputStream is = Resources.getResourceAsStream("myBatis-config.xml"); SqlSessionFactoryBuilder builderObj = new SqlSessionFactoryBuilder(); factory = builderObj.build(is); sqlSession = factory.openSession(); } @After public void destory(){ if(sqlSession!=null){ sqlSession.close(); } } @Test public void testSecondCache(){ //會話過程當中第一次發送請求,從數據庫中獲得結果 //獲得結果以後,mybatis自動將這個查詢結果放入到當前用戶的一級緩存 DeptDao dao = sqlSession.getMapper(DeptDao.class); Dept dept = dao.findByDeptNo(1); System.out.println("第一次查詢獲得部門對象 = "+dept); //觸發MyBatis框架從當前一級緩存中將Dept對象保存到二級緩存 sqlSession.commit(); // 改爲 sqlSession.close(); 效果相同 SqlSession session2 = factory.openSession(); DeptDao dao2 = session2.getMapper(DeptDao.class); Dept dept2 = dao2.findByDeptNo(1); System.out.println("第二次查詢獲得部門對象 = "+dept2); } }
測試二級緩存效果,提交事務,
sqlSession
查詢完數據後,sqlSession2
相同的查詢是否會從緩存中獲取數據。
測試結果以下:

經過結果能夠得知,首次執行的SQL語句是從數據庫中查詢獲得的結果,而後第一個 SqlSession 執行提交,第二個 SqlSession 執行相同的查詢後是從緩存中查取的。
用一下這幅圖可以比較直觀的反映兩次 SqlSession 的緩存命中
與一級緩存同樣,二級緩存也會存在失效的條件的,下面咱們就來探究一下哪些狀況會形成二級緩存失效
SqlSession 在未提交的時候,SQL 語句產生的查詢結果尚未放入二級緩存中,這個時候 SqlSession2 在查詢的時候是感覺不到二級緩存的存在的,修改對應的測試類,結果以下:
@Test public void testSqlSessionUnCommit(){ //會話過程當中第一次發送請求,從數據庫中獲得結果 //獲得結果以後,mybatis自動將這個查詢結果放入到當前用戶的一級緩存 DeptDao dao = sqlSession.getMapper(DeptDao.class); Dept dept = dao.findByDeptNo(1); System.out.println("第一次查詢獲得部門對象 = "+dept); //觸發MyBatis框架從當前一級緩存中將Dept對象保存到二級緩存 SqlSession session2 = factory.openSession(); DeptDao dao2 = session2.getMapper(DeptDao.class); Dept dept2 = dao2.findByDeptNo(1); System.out.println("第二次查詢獲得部門對象 = "+dept2); }
產生的輸出結果:
與一級緩存同樣,更新操做極可能對二級緩存形成影響,下面用三個 SqlSession來進行模擬,第一個 SqlSession 只是單純的提交,第二個 SqlSession 用於檢驗二級緩存所產生的影響,第三個 SqlSession 用於執行更新操做,測試以下:
@Test public void testSqlSessionUpdate(){ SqlSession sqlSession = factory.openSession(); SqlSession sqlSession2 = factory.openSession(); SqlSession sqlSession3 = factory.openSession(); // 第一個 SqlSession 執行更新操做 DeptDao deptDao = sqlSession.getMapper(DeptDao.class); Dept dept = deptDao.findByDeptNo(1); System.out.println("dept = " + dept); sqlSession.commit(); // 判斷第二個 SqlSession 是否從緩存中讀取 DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class); Dept dept2 = deptDao2.findByDeptNo(1); System.out.println("dept2 = " + dept2); // 第三個 SqlSession 執行更新操做 DeptDao deptDao3 = sqlSession3.getMapper(DeptDao.class); deptDao3.updateDept(new Dept(1,"ali","hz")); sqlSession3.commit(); // 判斷第二個 SqlSession 是否從緩存中讀取 dept2 = deptDao2.findByDeptNo(1); System.out.println("dept2 = " + dept2); }
對應的輸出結果以下
現有這樣一個場景,有兩個表,部門表dept(deptNo,dname,loc)和 部門數量表deptNum(id,name,num),其中部門表的名稱和部門數量表的名稱相同,經過名稱可以聯查兩個表能夠知道其座標(loc)和數量(num),如今我要對部門數量表的 num 進行更新,而後我再次關聯dept 和 deptNum 進行查詢,你認爲這個 SQL 語句可以查詢到的 num 的數量是多少?來看一下代碼探究一下
DeptNum.java
public class DeptNum { private int id; private String name; private int num; get and set... }
DeptVo.java
public class DeptVo { private Integer deptNo; private String dname; private String loc; private Integer num; public DeptVo(Integer deptNo, String dname, String loc, Integer num) { this.deptNo = deptNo; this.dname = dname; this.loc = loc; this.num = num; } public DeptVo(String dname, Integer num) { this.dname = dname; this.num = num; } get and set @Override public String toString() { return "DeptVo{" + "deptNo=" + deptNo + ", dname='" + dname + '\'' + ", loc='" + loc + '\'' + ", num=" + num + '}'; } }
DeptDao.java
public interface DeptDao { ... DeptVo selectByDeptVo(String name); DeptVo selectByDeptVoName(String name); int updateDeptVoNum(DeptVo deptVo); }
DeptDao.xml
<select id="selectByDeptVo" resultType="com.mybatis.beans.DeptVo"> select d.deptno,d.dname,d.loc,dn.num from dept d,deptNum dn where dn.name = d.dname and d.dname = #{name} </select> <select id="selectByDeptVoName" resultType="com.mybatis.beans.DeptVo"> select * from deptNum where name = #{name} </select> <update id="updateDeptVoNum" parameterType="com.mybatis.beans.DeptVo"> update deptNum set num = #{num} where name = #{dname} </update>
DeptNum 數據庫初始值:
測試類對應以下:
/** * 探究多表操做對二級緩存的影響 */ @Test public void testOtherMapper(){ // 第一個mapper 先執行聯查操做 SqlSession sqlSession = factory.openSession(); DeptDao deptDao = sqlSession.getMapper(DeptDao.class); DeptVo deptVo = deptDao.selectByDeptVo("ali"); System.out.println("deptVo = " + deptVo); // 第二個mapper 執行更新操做 並提交 SqlSession sqlSession2 = factory.openSession(); DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class); deptDao2.updateDeptVoNum(new DeptVo("ali",1000)); sqlSession2.commit(); sqlSession2.close(); // 第一個mapper 再次進行查詢,觀察查詢結果 deptVo = deptDao.selectByDeptVo("ali"); System.out.println("deptVo = " + deptVo); }
測試結果以下:
在對DeptNum 表執行了一次更新後,再次進行聯查,發現數據庫中查詢出的仍是 num 爲 1050 的值,也就是說,實際上 1050 -> 1000 ,最後一次聯查實際上查詢的是第一次查詢結果的緩存,而不是從數據庫中查詢獲得的值,這樣就讀到了髒數據。
解決辦法
若是是兩個mapper命名空間的話,可使用 <cache-ref>
來把一個命名空間指向另一個命名空間,從而消除上述的影響,再次執行,就能夠查詢到正確的數據
源碼模塊主要分爲兩個部分:二級緩存的建立和二級緩存的使用,首先先對二級緩存的建立進行分析:
二級緩存的建立是使用 Resource 讀取 XML 配置文件開始的
InputStream is = Resources.getResourceAsStream("myBatis-config.xml"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); factory = builder.build(is);
讀取配置文件後,須要對XML建立 Configuration並初始化
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse());
調用 parser.parse()
解析根目錄 /configuration 下面的標籤,依次進行解析
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; }
private void parseConfiguration(XNode root) { try { //issue #117 read properties first propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
其中有一個二級緩存的解析就是
mapperElement(root.evalNode("mappers"));
而後進去 mapperElement 方法中
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse();
繼續跟 mapperParser.parse() 方法
public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
這其中有一個 configurationElement 方法,它是對二級緩存進行建立,以下
private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } }
有兩個二級緩存的關鍵點
cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache"));
也就是說,mybatis 首先進行解析的是 cache-ref
標籤,其次進行解析的是 cache
標籤。
根據上面咱們的 — 多表操做對二級緩存的影響 一節中提到的解決辦法,採用 cache-ref 來進行命名空間的依賴可以避免二級緩存,可是總不能每次寫一個 XML 配置都會採用這種方式吧,最有效的方式仍是避免多表操做使用二級緩存
而後咱們再來看一下cacheElement(context.evalNode("cache")) 這個方法
private void cacheElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } }
認真看一下其中的屬性的解析,是否是感受很熟悉?這不就是對 cache 標籤屬性的解析嗎?!!!
上述最後一句代碼
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { Cache cache = new CacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass, PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass, LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); configuration.addCache(cache); currentCache = cache; return cache; }
這段代碼使用了構建器模式,一步一步構建Cache 標籤的全部屬性,最終把 cache 返回。
在 mybatis 中,使用 Cache 的地方在 CachingExecutor
中,來看一下 CachingExecutor 中緩存作了什麼工做,咱們以查詢爲例
@Override 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, parameterObject, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } // 委託模式,交給SimpleExecutor等實現類去實現方法。 return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
其中,先從 MapperStatement 取出緩存。只有經過<cache/>,<cache-ref/>
或@CacheNamespace,@CacheNamespaceRef
標記使用緩存的Mapper.xml或Mapper接口(同一個namespace,不能同時使用)纔會有二級緩存。
若是緩存不爲空,說明是存在緩存。若是cache存在,那麼會根據sql配置(<insert>,<select>,<update>,<delete>
的flushCache
屬性來肯定是否清空緩存。
flushCacheIfRequired(ms);
而後根據xml配置的屬性useCache
來判斷是否使用緩存(resultHandler通常使用的默認值,不多會null)。
if (ms.isUseCache() && resultHandler == null)
確保方法沒有Out類型的參數,mybatis不支持存儲過程的緩存,因此若是是存儲過程,這裏就會報錯。
private void ensureNoOutParams(MappedStatement ms, Object parameter, BoundSql boundSql) { if (ms.getStatementType() == StatementType.CALLABLE) { for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) { if (parameterMapping.getMode() != ParameterMode.IN) { throw new ExecutorException("Caching stored procedures with OUT params is not supported. Please configure useCache=false in " + ms.getId() + " statement."); } } } }
而後根據在 TransactionalCacheManager
中根據 key 取出緩存,若是沒有緩存,就會執行查詢,而且將查詢結果放到緩存中並返回取出結果,不然就執行真正的查詢方法。
List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list;
那麼究竟應該不該該使用二級緩存呢?先來看一下二級緩存的注意事項:
namespace
爲單位的,不一樣namespace
下的操做互不影響。namespace
下的所有緩存。namespace
。若是你遵照二級緩存的注意事項,那麼你就可使用二級緩存。
可是,若是不能使用多表操做,二級緩存不就能夠用一級緩存來替換掉嗎?並且二級緩存是表級緩存,開銷大,沒有一級緩存直接使用 HashMap 來存儲的效率更高,因此二級緩存並不推薦使用。