深入探究 MyBatis 緩存機(jī)制
在當(dāng)今的軟件開發(fā)領(lǐng)域,性能優(yōu)化始終是一個備受關(guān)注的核心議題。而在眾多提升性能的技術(shù)手段中,MyBatis 緩存無疑占據(jù)著重要的一席之地。當(dāng)我們深入探索 MyBatis 的世界時,會發(fā)現(xiàn)其緩存機(jī)制宛如一座隱藏的寶藏,蘊(yùn)含著巨大的潛力和價值。
在接下來的篇章中,我們將一同踏上探索 MyBatis 緩存的奇妙之旅。我們將逐步揭開它神秘的面紗,深入剖析其背后的原理、結(jié)構(gòu)和運作方式。通過了解它是如何巧妙地減少數(shù)據(jù)庫查詢次數(shù)、提升系統(tǒng)響應(yīng)速度,我們能更好地把握這一強(qiáng)大工具,為我們的開發(fā)項目帶來更卓越的性能表現(xiàn)。
一、詳解一級緩存
1. 什么是一級緩存
當(dāng)我們建立SqlSession時,就可以通過Mybatis進(jìn)行sql查詢,假如本次session查詢時我們需要進(jìn)行兩次相同的sql查詢,就需要進(jìn)行進(jìn)行兩次的磁盤IO,為了避免這種沒必要的等待,Mybatis為每一個SqlSession設(shè)置一級緩存,在同一個SqlSession中,一級緩存會將第一次查詢結(jié)果緩存起來,第二次相同的查詢就可以直接使用了。
2. 一級緩存使用示例
Mybatis默認(rèn)是開啟一級緩存的,如下所示,可以發(fā)現(xiàn)只要第二次使用的sql和參數(shù)一樣,就會從一級緩存中獲取數(shù)據(jù)。
User1 user1 = user1Mapper.select("1");
logger.info("一級緩存第一次查詢:[{}]", user1);
User1 user11 = user1Mapper.select("1");
logger.info("一級緩存第二次查詢:[{}]", user11);
User1 user12 = user1Mapper.select("2");
logger.info("一級緩存第三次查詢,id不同:[{}]", user12);
輸出結(jié)果:
2022-11-27 15:51:28,313 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-27 15:51:28,338 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-27 15:51:28,539 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - <== Total: 1
2022-11-27 15:51:28,541 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-27 15:51:28,541 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Parameters: 2(String)
[main] INFO com.sharkchili.mapper.MyBatisTest - 一級緩存第一次查詢:[User1{id='1', name='小明', user2=null}]
[main] INFO com.sharkchili.mapper.MyBatisTest - 一級緩存第二次查詢:[User1{id='1', name='小明', user2=null}]
2022-11-27 15:51:28,667 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - <== Total: 1
[main] INFO com.sharkchili.mapper.MyBatisTest - 一級緩存第三次查詢,id不同:[User1{id='2', name='小王', user2=null}]
3. 一級緩存的執(zhí)行過程
當(dāng)然我們也得有一個查詢代碼,查詢代碼如下所示:
User1 user1 = user1Mapper.select("1");
logger.info("一級緩存第一次查詢:[{}]", user1);
本質(zhì)上mapper代理對象進(jìn)行查詢操作時底層的BaseExecutor會調(diào)用queryFromDatabase獲取查詢結(jié)果,然后將查詢結(jié)果存到緩存中:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
List list;
try {
//執(zhí)行并獲取查詢結(jié)果
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}
//基于本次查詢用到的MappedStatement , 參數(shù), rowBounds, sql作為key將結(jié)果緩存
this.localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
this.localOutputParameterCache.putObject(key, parameter);
}
return list;
}
總結(jié)一下流程就如下圖所示:
4. 一級緩存的生命周期
- 當(dāng)SqlSession調(diào)用了close之后,會直接釋放PerpetualCache對象,緩存自然不能使用了。
- 進(jìn)行update、delete、insert等操作,緩存就會被清空,但是緩存對象還能用。
- 調(diào)用clearCache同理,緩存被清空,但是對象還能用。
二、詳解二級緩存
1. 什么是二級緩存
二級緩存是mybatis為了解決跨session緩存數(shù)據(jù)所增加的一層面向namespace級別的緩存方案,即以mapper文件為單位劃分的緩存空間,通過開啟二級緩存,程序執(zhí)行查詢時會優(yōu)先從全局共享的的二級緩存開始查詢,如果全局的二級緩存沒有數(shù)據(jù),再通過一級緩存查詢,如果有則返回并返回,如果沒有則執(zhí)行SQL查詢依次緩存到一級緩存、二級緩存中:
2. 二級緩存使用示例
為了討論二級緩存,我們不妨展示一個簡單的二級緩存配置示例,首先Mybatis配置開啟二級緩存,其實這個可以不用配置,默認(rèn)的情況下是true:
<settings>
<!--開啟二級緩存-->
<setting name="cacheEnabled" value="true"/>
</settings>
對應(yīng)的Mapper文件下添加下面這段配置:
<cache/>
然后我們給出對的測試代碼:
UserMapper userMapper = SpringUtil.getBean(UserMapper.class);
User user = new User();
user.setId(1L);
//第一次查詢
User u = userMapper.selectByUserId(user);
log.info("user:{}", JSONUtil.toJsonStr(u));
//第二次查詢
User u2 = userMapper.selectByUserId(user);
log.info("user:{}", JSONUtil.toJsonStr(u2));
可以看到使用同樣的會話,第二次查詢不會查詢SQL而是直接從二級緩存獲取數(shù)據(jù):
2024-12-13 12:13:00.458 INFO 17996 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2024-12-13 12:13:00.464 DEBUG 17996 --- [ main] c.s.mapper.UserMapper.selectByUserId : ==> Preparing: select u.id id, u.name name , m.total total from user u inner join money m on u.id = m.u_id where u.id = 1
2024-12-13 12:13:00.482 DEBUG 17996 --- [ main] c.s.mapper.UserMapper.selectByUserId : ==> Parameters:
2024-12-13 12:13:00.498 DEBUG 17996 --- [ main] c.s.mapper.UserMapper.selectByUserId : <== Total: 1
2024-12-13 12:13:00.549 INFO 17996 --- [ main] com.sharkChili.WebApplication : user:{"id":1,"name":"xiaoming","total":50}
2024-12-13 12:13:00.550 DEBUG 17996 --- [ main] com.sharkChili.mapper.UserMapper : Cache Hit Ratio [com.sharkChili.mapper.UserMapper]: 0.5
2024-12-13 12:13:00.550 INFO 17996 --- [ main] com.sharkChili.WebApplication : user:{"id":1,"name":"xiaoming","total":50}
3. 二級緩存的工作模式
在開啟二級緩存配置后,框架會首先去CachingExecutor看看是否有緩存數(shù)據(jù),若沒有則會從一級緩存查詢,實在找不到就通過BaseExecutor查詢并處理完緩存起來。
注意這里CachingExecutor用到了裝飾者模式,將Executor 組合進(jìn)來,所以CachingExecutor會先調(diào)用(List)this.tcm.getObject(cache, key);看看緩存中是否有數(shù)據(jù),若沒有在進(jìn)行進(jìn)一步查詢并緩存的操作。
//將基礎(chǔ)執(zhí)行器作為被裝飾的成員屬性組合進(jìn)來
private final Executor delegate;
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) {
this.flushCacheIfRequired(ms);
//開啟二級緩存則執(zhí)行該邏輯
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
//先去緩存查詢
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
// 若為空則調(diào)用BaseExecutor 進(jìn)行數(shù)據(jù)獲取
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//將數(shù)據(jù)存到二級緩存中
this.tcm.putObject(cache, key, list);
}
return list;
}
}
//調(diào)用BaseExecutor 獲取查詢結(jié)果并緩存
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
4. 二級緩存怎么作用域
二級緩存怎么作用域有兩種:
- 自定義劃分,我們在每個Mapper.xml中添加 <cache/>使得每一個mapper都有一個全局的獨立緩存空間:
- 假如我們希望多個mapper共享一個空間的話,需要被分享的mapper使用<cache/>,而其他mapper則用<cache-ref namespace="">指向這個空間即可。
5. 使用二級緩存要具備的幾個條件
總的來說是三個條件:
- 全局配置開啟二級緩存:<setting name="cacheEnabled" value="true"/>,默認(rèn)是true的。
- mapper.xml標(biāo)簽配置了 <cache/>或者 <cache-ref/>
- select語句配置useCache=true
6. 二級緩存實現(xiàn)的選擇有哪些和默認(rèn)項
有三種吧:
- 框架自身提供了很多緩存方案,這些緩存還提供了不同的回收策略:例如LRU、FIFO等。
- 用戶繼承接口org.apache.ibatis.cache.Cache自行實現(xiàn)一個緩存。
- 通過第三方緩存工具集成。
對于Mybatis的二級緩存默認(rèn)緩存算法,如下圖,可以看到框架自身基于裝飾者模式實現(xiàn)了很多緩存工具,并且每個緩存容量都有限制,不同的緩存工具內(nèi)存回收策略是不同的:例如LruCache即最近最少使用算法,內(nèi)存容量滿了就回收到現(xiàn)在為止最不常用的。而FifoCache同理,內(nèi)存滿了之后回收最先被緩存的數(shù)據(jù),ScheduledCache則是定時清理緩存了。
7. 二級緩存關(guān)聯(lián)刷新問題
我們直接從一個比較實際的場景出發(fā),首先我們有一張user表,里面有一條id為1的用戶數(shù)據(jù),name是xiaoming,然后有一張關(guān)聯(lián)表money,它記錄xiaoming的錢包金額為50,對應(yīng)數(shù)據(jù)信息如下:
-- SELECT * FROM `user` u ;
id|name |
--+--------+
1|xiaoming|
-- SELECT * FROM money m ;
id|u_id|total|
--+----+-----+
1| 1| 10|
然后我們在userMapper中寫了這樣一條關(guān)聯(lián)查詢的SQL并開啟二級緩存:
<select id="selectByUserId" resultType="com.sharkChili.domain.User">
select u.id id, u.name name , m.total total
from user u
inner join money m on u.id = m.u_id
where u.id = #{id}
</select>
然后我們執(zhí)行下面這段操作:
- 通過關(guān)聯(lián)查詢獲取用戶1的姓名和關(guān)聯(lián)表的金額信息。
- 通過moneyMapper更新用戶1對應(yīng)余額。
- 通過二級緩再次查詢。
那么問題來了,第二次查詢的金額會是更新后的10嗎?
UserMapper userMapper = SpringUtil.getBean(UserMapper.class);
User user = new User();
user.setId(1L);
//第一次查詢
User u = userMapper.selectByUserId(user);
log.info("user:{}", JSONUtil.toJsonStr(u));
//更新用戶1對應(yīng)的余額信息
MoneyMapper moneyMapper = SpringUtil.getBean(MoneyMapper.class);
Money money = new Money();
money.setId(1L);
money.setTotal(10L);
moneyMapper.updateByPrimaryKeySelective(money);
//第二次查詢
User u2 = userMapper.selectByUserId(user);
log.info("user:{}", JSONUtil.toJsonStr(u2));
答案是還是走了臟緩存:
2024-12-13 12:22:09.286 DEBUG 9056 --- [ main] c.s.mapper.UserMapper.selectByUserId : ==> Preparing: select u.id id, u.name name , m.total total from user u inner join money m on u.id = m.u_id where u.id = ?
2024-12-13 12:22:09.307 DEBUG 9056 --- [ main] c.s.mapper.UserMapper.selectByUserId : ==> Parameters: 1(Long)
2024-12-13 12:22:09.325 DEBUG 9056 --- [ main] c.s.mapper.UserMapper.selectByUserId : <== Total: 1
2024-12-13 12:22:09.379 INFO 9056 --- [ main] com.sharkChili.WebApplication : user:{"id":1,"name":"xiaoming","total":50}
2024-12-13 12:22:09.394 DEBUG 9056 --- [ main] c.s.m.M.updateByPrimaryKeySelective : ==> Preparing: update money SET total = ? where id = ?
2024-12-13 12:22:09.394 DEBUG 9056 --- [ main] c.s.m.M.updateByPrimaryKeySelective : ==> Parameters: 10(Long), 1(Long)
2024-12-13 12:22:09.402 DEBUG 9056 --- [ main] c.s.m.M.updateByPrimaryKeySelective : <== Updates: 1
2024-12-13 12:22:09.403 DEBUG 9056 --- [ main] com.sharkChili.mapper.UserMapper : Cache Hit Ratio [com.sharkChili.mapper.UserMapper]: 0.5
2024-12-13 12:22:09.403 INFO 9056 --- [ main] com.sharkChili.WebApplication : user:{"id":1,"name":"xiaoming","total":50}
原因也很簡單,二級緩存是以namespace為區(qū)域劃分,這意味著userMapper緩存的數(shù)據(jù)不會因為moneyMapper的改變而觸發(fā)更新,這意味著如果涉及關(guān)聯(lián)查詢的緩存數(shù)據(jù)可能會因為關(guān)聯(lián)表的更新無法感知而出現(xiàn)臟緩存:
解決方案也很簡單,我們只要確保緩存更新被關(guān)聯(lián)表時,及時刷新響應(yīng)緩存即可,具體可以參考這篇文章
MyBatis 二級緩存 關(guān)聯(lián)刷新實現(xiàn):https://blog.csdn.net/qq_37217713/article/details/123288123
8. 二級緩存的配置參數(shù)
主要參數(shù)有這么四個:
- 緩存回收策略(eviction):這個參數(shù)有這么4個LRU最近最少回收算法這種是默認(rèn)的算法、FIFO先進(jìn)先出算法、SOFT算法(基于垃圾回收器算法和軟引用回收的對象)、WEAK算法即基于垃圾回收器算法和弱引用規(guī)則回收對象。
- 刷新間隔(flushInterval):單位毫秒。
- 容量(size):引用數(shù)目,正整數(shù)。
- 是否只讀(readOnly):如果只讀則直接返回緩存實例,性能上會相對有些優(yōu)勢。若不為只讀則會通過序列化獲取對象的拷貝,性能就相對差一些。
配置范例如下所示:
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
9. 二級緩存的失效場景
有兩種情況一種是第一次查詢的sqlsession沒有提交或者關(guān)閉:
User1 user1 = user1Mapper.select("1");
logger.info("二級緩存第一次查詢:[{}]", user1);
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User1Mapper user1Mapper1 = sqlSession2.getMapper(User1Mapper.class);
User1 user13 = user1Mapper1.select("1");
logger.info("二級緩存第二次查詢:[{}]", user13);
輸出結(jié)果:
2022-11-29 01:05:43,339 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-29 01:05:43,363 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-29 01:05:43,502 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - <== Total: 1
2022-11-29 01:05:43,506 [main] DEBUG [com.sharkchili.mapper.User1Mapper] - Cache Hit Ratio [com.sharkchili.mapper.User1Mapper]: 0.0
2022-11-29 01:05:43,506 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
[main] INFO com.sharkchili.mapper.MyBatisTest - 二級緩存第一次查詢:[User1{id='1', name='小明', user2=null}]
2022-11-29 01:05:44,234 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 550668305.
2022-11-29 01:05:44,234 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@20d28811]
2022-11-29 01:05:44,351 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-29 01:05:44,351 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-29 01:05:44,465 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - <== Total: 1
[main] INFO com.sharkchili.mapper.MyBatisTest - 二級緩存第二次查詢:[User1{id='1', name='小明', user2=null}]
第二種則是常規(guī)更新操作:
2022-11-29 01:07:22,302 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-29 01:07:22,326 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-29 01:07:22,456 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - <== Total: 1
[main] INFO com.sharkchili.mapper.MyBatisTest - 二級緩存第一次查詢:[User1{id='1', name='小明', user2=null}]
2022-11-29 01:07:22,479 [main] DEBUG [com.sharkchili.mapper.User1Mapper.updatebySet] - ==> Preparing: update user1 SET id=?, name=? where id=?
2022-11-29 01:07:22,479 [main] DEBUG [com.sharkchili.mapper.User1Mapper.updatebySet] - ==> Parameters: 1(String), aa(String), 1(String)
2022-11-29 01:07:22,713 [main] DEBUG [com.sharkchili.mapper.User1Mapper.updatebySet] - <== Updates: 1
2022-11-29 01:07:22,714 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Rolling back JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:22,833 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Returned connection 260840925 to pool.
2022-11-29 01:07:22,949 [main] DEBUG [com.sharkchili.mapper.User1Mapper] - Cache Hit Ratio [com.sharkchili.mapper.User1Mapper]: 0.0
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Checked out connection 260840925 from pool.
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:23,065 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-29 01:07:23,065 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Parameters: 1(String)
[main] INFO com.sharkchili.mapper.MyBatisTest - 二級緩存第二次查詢:[User1{id='1', name='小明', user2=null}]
2022-11-29 01:07:23,184 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - <== Total: 1
要想真正用上二級緩存,需要像這樣及時提交或者關(guān)閉其他session:
User1 user1 = user1Mapper.select("1");
logger.info("二級緩存第一次查詢:[{}]", user1);
if (sqlSession != null) {
sqlSession.close();
}
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User1Mapper user1Mapper1 = sqlSession2.getMapper(User1Mapper.class);
User1 user13 = user1Mapper1.select("1");
logger.info("二級緩存第二次查詢:[{}]", user13);
if (sqlSession2 != null) {
sqlSession2.close();
}
10. Mybatis一級緩存和二級緩存的區(qū)別
一級緩存默認(rèn)開啟,作用域session,當(dāng)session調(diào)用close或者flush時就會被清空,緩存也是PerpetualCache 一種基于HashMap實現(xiàn)的緩存。 而二級緩存作用于mapper(namespace),也是基于緩存也是PerpetualCache ,默認(rèn)不開啟,需要緩存的屬性類必須實現(xiàn)序列化接口即繼承Serializable,而且二級緩存可以自定義緩存存儲源。