MyBatis 二級快取全詳解
目錄
- MyBatis 二級快取介紹
- 二級快取開啟條件
- 探究二級快取
- 二級快取失效的條件
- 第一次SqlSession 未提交
- 更新對二級快取影響
- 探究多表操作對二級快取的影響
- 二級快取原始碼解析
- 二級快取的建立
- 二級快取的使用
- 是否應該使用二級快取?
- MyBatis 二級快取介紹
我們在上一篇文章 ( https://mp.weixin.qq.com/s/4Puee_pPCNArkgnFaYlIjg ) 介紹了 MyBatis 的一級快取的作用,如何開啟,一級快取的本質是什麼,一級快取失效的原因是什麼? MyBatis 只有一級快取嗎?來找找答案吧!
MyBatis 二級快取介紹
上一篇文章中我們介紹到了 MyBatis 一級快取其實就是 SqlSession 級別的快取,什麼是 SqlSession 級別的快取呢?一級快取的本質是什麼呢? 以及一級快取失效的原因?我希望你在看下文之前能夠回想起來這些內容。
MyBatis 一級快取最大的共享範圍就是一個SqlSession內部,那麼如果多個 SqlSession 需要共享快取,則需要開啟二級快取,開啟二級快取後,會使用 CachingExecutor 裝飾 Executor,進入一級快取的查詢流程前,先在CachingExecutor 進行二級快取的查詢,具體的工作流程如下所示
當二級快取開啟後,同一個名稱空間(namespace) 所有的操作語句,都影響著一個共同的 cache,也就是二級快取被多個 SqlSession 共享,是一個全域性的變數。當開啟快取後,資料的查詢執行的流程就是 二級快取 -> 一級快取 -> 資料庫。
二級快取開啟條件
二級快取預設是不開啟的,需要手動開啟二級快取,實現二級快取的時候,MyBatis要求返回的POJO必須是可序列化的。開啟二級快取的條件也是比較簡單,通過直接在 MyBatis 配置檔案中通過
<settings>
<setting name = "cacheEnabled" value = "true" />
</settings>
來開啟二級快取,還需要在 Mapper 的xml 配置檔案中加入 <cache>
標籤
設定 cache 標籤的屬性
cache 標籤有多個屬性,一起來看一些這些屬性分別代表什麼意義
eviction
: 快取回收策略,有這幾種回收策略- LRU - 最近最少回收,移除最長時間不被使用的物件
- FIFO - 先進先出,按照快取進入的順序來移除它們
- SOFT - 軟引用,移除基於垃圾回收器狀態和軟引用規則的物件
- WEAK - 弱引用,更積極的移除基於垃圾收集器和弱引用規則的物件
預設是 LRU 最近最少回收策略
flushinterval
快取重新整理間隔,快取多長時間重新整理一次,預設不清空,設定一個毫秒值readOnly
: 是否只讀;true 只讀,MyBatis 認為所有從快取中獲取資料的操作都是隻讀操作,不會修改資料。MyBatis 為了加快獲取資料,直接就會將資料在快取中的引用交給使用者。不安全,速度快。讀寫(預設):MyBatis 覺得資料可能會被修改size
: 快取存放多少個元素type
: 指定自定義快取的全類名(實現Cache 介面即可)blocking
: 若快取中找不到對應的key,是否會一直blocking,直到有對應的資料進入快取。
探究二級快取
我們繼續以 MyBatis 一級快取文章中的例子為基礎,搭建一個滿足二級快取的例子,來對二級快取進行探究,例子如下(對 一級快取的例子部分原始碼進行修改):
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 未提交
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
下的操作互不影響。 - insert,update,delete操作會清空所在
namespace
下的全部快取。 - 通常使用MyBatis Generator生成的程式碼中,都是各個表獨立的,每個表都有自己的
namespace
。 - 多表操作一定不要使用二級快取,因為多表操作進行更新操作,一定會產生髒資料。
如果你遵守二級快取的注意事項,那麼你就可以使用二級快取。
但是,如果不能使用多表操作,二級快取不就可以用一級快取來替換掉嗎?而且二級快取是表級快取,開銷大,沒有一級快取直接使用 HashMap 來儲存的效率更高,所以二級快取並不推薦使用。
文章參考:
聊聊MyBatis快取機制
mybatis一級快取二級快取
深入瞭解MyBatis二級快取 https://blog.csdn.net/isea533/article/details/4456