MyBatis-23MyBatis快取配置【二級快取】
概述
這裡我們來看下工作中最常用的二級快取。
MyBaits的二級快取可以理解為存在於SqlSessionFactory的生命週期中。
目前還沒接觸過同時存在多個SqlSessionFactory的情況,但可以知道當存在多個SqlSessionFactory時,他們的快取物件都是繫結在各自物件上的,快取資料在一般情況下是不相通的。 只有在使用如redis這樣的快取資料庫時,才可以共享快取。
二級快取的配置
全域性開關cacheEnabled
在MyBatis的全域性配置settings中有一個引數 cacheEnabled , 這個引數是二級快取的全域性開關,預設為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(可讀/可寫)的,意味著物件檢索不是共享的,而且可以安全的被呼叫者修改,而不干擾其他呼叫者或者執行緒所做的潛在修改。
所有的這些屬性都是可以通過快取元素的屬性來修改,比如
<cache
eviction="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">
SELECT
id,
privilege_name privilegeName,
privilege_url privilegeUrl
FROM
sys_privilege
WHERE
id = #{id}
</select>
單元測試
@Test
public 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 Priv
Assert.assertEquals("New Priv", sysPrivilege2.getPrivilegeName());
// 這裡的sysPrivilege2 和 前一個session中的sysPrivilege不是同一個例項
Assert.assertNotEquals(sysPrivilege, sysPrivilege2);
// 獲取sysPrivilege3
SysPrivilege 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>
)來避免髒資料,那就失去了快取的意義。
實際上就是說,二級快取不能用
如何挽救二級快取
想更高效率的使用二級快取是解決不了的
建議放棄二級快取,在業務層使用可控制的快取代替更好。