MyBatis-23MyBatis缓存配置【二级缓存】
文章目錄
- 概述
- 二級緩存的配置
- 全局開關cacheEnabled
- Mapper.xml中配置二級緩存
- Mapper接口中配置二級緩存
- 只使用注解方式配置二級緩存
- 同時使用注解方式和XML映射文件時
- 二級緩存的使用
- 前提:實體類實現Serializable接口
- 示例
- 實體類SysPrivilege實現Serializable接口
- PrivilegeMapper接口類增加接口方法
- PrivilegeMapper.xml中配置對單表操作的SQL
- 單元測試
- 注意事項(重要)
- MyBatis二級緩存的使用場景
- 避免使用二級緩存
- 為什么避免使用二級緩存
- 多表操作一定不能使用緩存
- 如何挽救二級緩存
概述
MyBatis-22MyBatis緩存配置【一級緩存】 中介紹了MyBatis默認的一級緩存,了解即可。
這里我們來看下工作中最常用的二級緩存。
MyBaits的二級緩存可以理解為存在于SqlSessionFactory的生命周期中。
目前還沒接觸過同時存在多個SqlSessionFactory的情況,但可以知道當存在多個SqlSessionFactory時,他們的緩存對象都是綁定在各自對象上的,緩存數據在一般情況下是不相通的。 只有在使用如redis這樣的緩存數據庫時,才可以共享緩存。
二級緩存的配置
全局開關cacheEnabled
在MyBatis的全局配置settings中有一個參數 cacheEnabled , 這個參數是二級緩存的全局開關,默認為true,初始狀態為啟用狀態。 如果設置為false ,即使后面的二級緩存配置,也不會生效。 默認為true,所以不用配置。
MyBatis的二級緩存是和命名空間綁定的,即二級緩存需要配置在Mapper.xml映射文件中或者配置在Mapper.java接口中。 在映射文件中,命名空間就是XML根節點mapper的namespace屬性。 在Mapper接口中,命名空間就是接口的全限定名稱。
Mapper.xml中配置二級緩存
在保證二級緩存全局配置開啟的情況下,如果想要給PrivilegeMapper.xml開啟二級緩存只需要在PrivilegeMapper.xml中添加 <cache/>元素即可。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><!-- 當Mapper接口和XML文件關聯的時候, namespace的值就需要配置成接口的全限定名稱 --> <mapper namespace="com.artisan.mybatis.xml.mapper.PrivilegeMapper"><cache/><!-- 其他配置 --> </mapper>默認的二級緩存功能如下:
-
映射語句文件中所有的select語句將會被緩存
-
映射語句文件中所有的insert update delete 語句會刷新緩存
-
緩存會使用(Least Flush Interval,LRU最近最少使用的)算法來收回
-
根據時間表(如 no Flush Interval,沒有刷新間隔),緩存不會以任何時間順序來刷新
-
緩存會存儲集合或對象(無論查詢方法返回什么類型的值)的1024個引用
-
緩存會被視為read/wriete(可讀/可寫)的,意味著對象檢索不是共享的,而且可以安全的被調用者修改,而不干擾其他調用者或者線程所做的潛在修改。
所有的這些屬性都是可以通過緩存元素的屬性來修改,比如
<cacheeviction="FIFO"flushInterval="60000"size="512"readOnly="true"/>這個更高級的配置創建了一個FIFP緩存,每隔60S刷新一次,存儲集合或對象的512個引用,而且返回的對象被認為是只讀的,因而在不同線程中的調用者之間修改它們會導致沖突。
cache可以配置的屬性如下:
- eviction(收回策略)
- LRU 最近最少使用的,移除最長時間不被使用的對象,這是默認值
- FIFO 先進先出,按對象進入緩存的順序來移除它們
- SOFT 軟引用,移除基于垃圾回收器狀態和軟引用規則的對象
- WEAK 弱引用,更積極的移除基于垃圾收集器狀態和弱引用規則的對象
-
flushInterval(刷新間隔)可以被設置為任意的正整數,而且它們代表一個合理的毫秒形式的時間段。 默認情況不設置,即沒有刷新間隔,緩存僅僅在調用語句時刷新
-
size(引用數目)可以被設置為任意的正整數,要記住緩存的對象數目和運行環境的可用內存資源數目,默認1024
-
readOnly(只讀)屬性可以被設置為true后者false。 只讀的緩存會給所有調用者返回緩存對象的相同實例,因此這些對象不能被修改,這提供了很重要的性能優勢。 可讀寫的緩存會通過序列化返回緩存對象的拷貝,這種方式會慢一些,但很安全,因此默認為false
Mapper接口中配置二級緩存
使用注解的方式時,如果想對注解方式啟用二級緩存,還需要在Mapper接口中進行配置,如果Mapper接口也存在對應的XML映射文件,兩者同時開啟緩存時,還需要特殊配置。
只使用注解方式配置二級緩存
當只使用注解方式配置二級緩存時,比如RoleMapper接口中,則需要增加如下配置
@CacheNamespace public interface RoleMapper {.... }只需要增加@CacheNamespace(org.apache.ibatis.annotations.CacheNamespace),該注解同樣可以配置各項屬性
@CacheNamespace{eviction = FifoCache.class,flushInterval = 60000,size = 512,readWrite = true }這里的readWrite屬性和XML中的readOnly屬性一樣,用于配置緩存是否為只讀類型,在這里true為讀寫,false為只讀,默認為true。
同時使用注解方式和XML映射文件時
當同時使用注解方式和XML映射文件時,如果同時配置了上述的二級緩存(使用xml 以及接口上標注了@CacheNamespace),會拋出如下異常
Cache collection already contains value for **這是因為Mapper接口和對應的XML文件是相同的命名空間,想使用二級緩存,兩者必須同時配置(如果接口不存在使用注解方式的方法,可以只在XML中配置),因此按照上面的方式進行配置就出錯,這個時候應該使用參照緩存,
在Mapper接口中,參照緩存配置如下
@CacheNamespaceRef(RoleMapper.class) public interface RoleMapper{}MyBatis很少會同時使用Mapper接口注解方式和XML映射文件,所以參照緩存并不是為了解決這個問題而設計的,參照緩存主要是為了解決臟讀
二級緩存的使用
前提:實體類實現Serializable接口
由于MyBatis配置的是可讀寫的緩存,而MyBatis使用SerializedCache序列化緩存來實現可讀寫緩存類,并通過序列化和反序列化來保證通過緩存獲取數據時,得到的是一個新的示例。 因此如果配置為只讀緩存,MyBatis會使用Map來存儲緩存值,這種情況下,從緩存中獲取的對象就是同一個實例。
因為使用可讀寫緩存,可以使用SerializedCache序列化緩存,這個緩存類要求所有被序列化的對象必須實現java.io.Serializable接口,所以需要修改SysPrivilege實體類
public class SysPrivilege implements Serializable {private static final long serialVersionUID = 6315662516417216377L;// 其他保持不變 }示例
實體類SysPrivilege實現Serializable接口
public class SysPrivilege implements Serializable {private static final long serialVersionUID = 6315662516417216377L;// 其他保持不變 }PrivilegeMapper接口類增加接口方法
/*** * * @Title: selectPrivilegeByIdWithCache* * @Description: 二級緩存測試方法 ,實體類SysPrivilege必須要實現Serializable* * @param id* @return* * @return: SysPrivilege*/SysPrivilege selectPrivilegeByIdWithCache(Long id);PrivilegeMapper.xml中配置對單表操作的SQL
<select id="selectPrivilegeByIdWithCache" resultType="com.artisan.mybatis.xml.domain.SysPrivilege">SELECTid,privilege_name privilegeName,privilege_url privilegeUrlFROMsys_privilegeWHEREid = #{id}</select>單元測試
@Testpublic void selectPrivilegeByIdWithCacheTest() {logger.info("selectPrivilegeByIdWithCacheTest");SqlSession sqlSession = getSqlSession();SysPrivilege sysPrivilege = null;try {// 獲取接口PrivilegeMapper privilegeMapper = sqlSession.getMapper(PrivilegeMapper.class);// 調用接口方法sysPrivilege = privilegeMapper.selectPrivilegeByIdWithCache(1L);sysPrivilege.setPrivilegeName("New Priv");// 再次調用相同的接口方法,查詢相同的用戶logger.info("再次調用相同的接口方法,查詢相同的用戶 Begin");SysPrivilege sysPrivilege2 = privilegeMapper.selectPrivilegeByIdWithCache(1L);logger.info("再次調用相同的接口方法,查詢相同的用戶 End");// 一級緩存在同一個sqlSession中,雖然沒有更新數據庫,但是會使用一級緩存Assert.assertEquals("New Priv", sysPrivilege2.getPrivilegeName());// sysPrivilege 和 sysPrivilege2 是同一個實例Assert.assertEquals(sysPrivilege, sysPrivilege2);} finally {// sqlSession關閉后,在二級緩存開啟的前提下,會寫入二級緩存sqlSession.close();}logger.info("重新獲取一個SqlSession");sqlSession = getSqlSession();try {// 獲取接口PrivilegeMapper privilegeMapper = sqlSession.getMapper(PrivilegeMapper.class);// 調用接口方法SysPrivilege sysPrivilege2 = privilegeMapper.selectPrivilegeByIdWithCache(1L);sysPrivilege.setPrivilegeName("New Priv");// 第二個session獲取的權限名為 New PrivAssert.assertEquals("New Priv", sysPrivilege2.getPrivilegeName());// 這里的sysPrivilege2 和 前一個session中的sysPrivilege不是同一個實例Assert.assertNotEquals(sysPrivilege, sysPrivilege2);// 獲取sysPrivilege3SysPrivilege sysPrivilege3 = privilegeMapper.selectPrivilegeByIdWithCache(1L);// 這里的sysPrivilege2 和sysPrivilege3是兩個不同的實例Assert.assertNotEquals(sysPrivilege2, sysPrivilege3);} finally {// sqlSession關閉后,在二級緩存開啟的前提下,會寫入二級緩存sqlSession.close();}}日志
2018-05-07 14:21:22,949 INFO [main] (BaseMapperTest.java:26) - sessionFactory bulit successfully 2018-05-07 14:21:22,949 INFO [main] (BaseMapperTest.java:29) - reader close successfully 2018-05-07 14:21:22,960 INFO [main] (PrivilegeMapperTest.java:38) - selectPrivilegeByIdWithCacheTest 2018-05-07 14:21:23,010 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.0 2018-05-07 14:21:23,080 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Preparing: SELECT id, privilege_name privilegeName, privilege_url privilegeUrl FROM sys_privilege WHERE id = ? 2018-05-07 14:21:23,270 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Parameters: 1(Long) 2018-05-07 14:21:23,320 TRACE [main] (BaseJdbcLogger.java:151) - <== Columns: id, privilegeName, privilegeUrl 2018-05-07 14:21:23,320 TRACE [main] (BaseJdbcLogger.java:151) - <== Row: 1, 用戶管理, /users 2018-05-07 14:21:23,340 DEBUG [main] (BaseJdbcLogger.java:145) - <== Total: 1 2018-05-07 14:21:23,340 INFO [main] (PrivilegeMapperTest.java:48) - 再次調用相同的接口方法,查詢相同的用戶 Begin 2018-05-07 14:21:23,340 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.0 2018-05-07 14:21:23,340 INFO [main] (PrivilegeMapperTest.java:50) - 再次調用相同的接口方法,查詢相同的用戶 End 2018-05-07 14:21:23,350 INFO [main] (PrivilegeMapperTest.java:60) - 重新獲取一個SqlSession 2018-05-07 14:21:23,360 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.3333333333333333 2018-05-07 14:21:23,360 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.5日志分析:
第一部分第一次和第二次查詢的sysPrivilege 和 sysPrivilege2 是完全相同的實例,這是使用的是默認的一級緩存,所以返回同一個實例。
當調用close方法關閉sqlSession后,sqlSession才回保存查詢數據到二級緩存中。 在這之后二級緩存才有了緩存數據。 所以第一次里看到的兩次查詢時,命中率都是0 。
重新開一個sqlSession,再次獲取sysPrivilege 時,因為緩存中有了數據,沒有查詢數據庫,而是輸出了命中率,這是的命中率為 0.3333333333333333 , 查詢3次,命中1次,因此是1/3
緊接著第4次查詢,加上上次的命中,2/4 ,命中率為0.5
注意: 我們為了測試,在獲取到數據后,調用了setPrivilegeName方法,實際中并不需要,避免人為產生臟數據。
注意事項(重要)
MyBatis二級緩存的使用場景
只能在【只有單表操作】的表上使用緩存,不只是要保證這個表在整個系統中只有單表操作,而且和該表有關的全部操作必須全部在一個namespace下。
在可以保證查詢遠遠大于insert,update,delete操作的情況下使用緩存,這一點需要保證在1的前提下才可以!
避免使用二級緩存
二級緩存帶來的好處遠遠比不上他所隱藏的危害。
二級緩存
- 緩存是以namespace為單位的,不同namespace下的操作互不影響。
- insert,update,delete操作會清空所在namespace下的全部緩存。
- 通常使用MyBatis Generator生成的代碼中,都是各個表獨立的,每個表都有自己的namespace。
為什么避免使用二級緩存
在符合【MyBatis二級緩存的使用場景】的要求時,并沒有什么危害。
其他情況就會有很多危害了。
針對一個表的某些操作不在他獨立的namespace下進行。
例如在UserMapper.xml中有大多數針對user表的操作。但是在一個XXXMapper.xml中,還有針對user單表的操作。
這會導致user在兩個命名空間下的數據不一致。如果在UserMapper.xml中做了刷新緩存的操作,在XXXMapper.xml中緩存仍然有效,如果有針對user的單表查詢,使用緩存的結果可能會不正確。
更危險的情況是在XXXMapper.xml做了insert,update,delete操作時,會導致UserMapper.xml中的各種操作充滿未知和風險。
有關這樣單表的操作可能不常見.
多表操作一定不能使用緩存
首先不管多表操作寫到那個namespace下,都會存在某個表不在這個namespace下的情況。
例如兩個表:role和user_role,如果想查詢出某個用戶的全部角色role,就一定會涉及到多表的操作。
<select id="selectUserRoles" resultType="UserRoleVO">select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid} </select>像上面這個查詢,你會寫到那個xml中呢??
不管是寫到RoleMapper.xml還是UserRoleMapper.xml,或者是一個獨立的XxxMapper.xml中。如果使用了二級緩存,都會導致上面這個查詢結果可能不正確。
如果你正好修改了這個用戶的角色,上面這個查詢使用緩存的時候結果就是錯的。
這點應該很容易理解。
在我看來,就以MyBatis目前的緩存方式來看是無解的。多表操作根本不能緩存。
如果你讓他們都使用同一個namespace(通過<cache-ref>)來避免臟數據,那就失去了緩存的意義。
實際上就是說,二級緩存不能用
如何挽救二級緩存
想更高效率的使用二級緩存是解決不了的
建議放棄二級緩存,在業務層使用可控制的緩存代替更好。
更多注意事項請參考劉老師的博客深入了解MyBatis二級緩存
總結
以上是生活随笔為你收集整理的MyBatis-23MyBatis缓存配置【二级缓存】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MyBatis-22MyBatis缓存配
- 下一篇: MyBatis-24MyBatis缓存配