SpringBoot下Mybatis的緩存的實(shí)現(xiàn)步驟
說(shuō)起 mybatis,作為 Java 程序員應(yīng)該是無(wú)人不知,它是常用的數(shù)據(jù)庫(kù)訪問(wèn)框架。與 Spring 和 Struts 組成了 Java Web 開(kāi)發(fā)的三劍客--- SSM。當(dāng)然隨著 Spring Boot 的發(fā)展,現(xiàn)在越來(lái)越多的企業(yè)采用的是 SpringBoot + mybatis 的模式開(kāi)發(fā),我們公司也不例外。而 mybatis 對(duì)于我也僅僅停留在會(huì)用而已,沒(méi)想過(guò)怎么去了解它,更不知道它的緩存機(jī)制了,直到那個(gè)生死難忘的 BUG。故事的背景比較長(zhǎng),但并不是啰嗦,只是讓讀者知道這個(gè) BUG 觸發(fā)的場(chǎng)景,加深記憶。在遇到類似問(wèn)題時(shí),可以迅速定位。
先說(shuō)下故事的前提,為了防止用戶在動(dòng)態(tài)中輸入特殊字符,用戶的動(dòng)態(tài)都是編碼后發(fā)到后臺(tái),而后臺(tái)在存入到 DB 表之前會(huì)解碼以方便在 DB 中查看以及上報(bào)到搜索引擎。在查詢用戶動(dòng)態(tài)的時(shí)候先從 DB 表中讀取并在后臺(tái)做一次編碼再傳到前端,前端再解碼就可以正常展示了。流程如下圖:

有一天后端預(yù)發(fā)環(huán)境發(fā)布完畢后,用戶的動(dòng)態(tài)頁(yè)面有的動(dòng)態(tài)顯示正常,而有的卻是被編碼過(guò)的??吹浆F(xiàn)象后的第一個(gè)反應(yīng)就是有問(wèn)題的動(dòng)態(tài)被編碼了兩次,但是編碼操作只會(huì)在 service 層的 findById 中有。理論不會(huì)在上層犯這種低級(jí)錯(cuò)誤。話不多說(shuō)便開(kāi)始排查新增加的代碼,發(fā)現(xiàn)只要進(jìn)入了新增加代碼中的某個(gè) if 分支則被編碼了兩次。分支中除了再次調(diào)用 findById(必要性不討論),也無(wú)其他特殊代碼了。百思不得其解后請(qǐng)教了旁邊的老司機(jī),老司機(jī)說(shuō)可能是 mybatis 緩存。于是看了下我代碼,將編碼的操作從 findById 中移出來(lái)后再次發(fā)布到預(yù)發(fā),正常了,心想老司機(jī)不愧是老司機(jī)。本次 BUG 觸發(fā)的有兩個(gè)條件需要注意:
- 整個(gè)操作過(guò)程都在一個(gè)函數(shù)中,而函數(shù)上面加了 @Transactional 的注解(對(duì) mybatis 來(lái)說(shuō)是在同一個(gè) SESSION 中)
- 一般只會(huì)調(diào)用 findByIdy 一次,如果進(jìn)入分支則會(huì)調(diào)用兩次 (第一次調(diào)用后做了編碼后被緩存,第二次從緩存讀后繼續(xù)被編碼)
便開(kāi)始谷歌 mybatis 的緩存機(jī)制,搜到了一篇非常不錯(cuò)的文章《聊聊 mybatis 的緩存機(jī)制 》,推薦大家看一下。但是這篇文章講到了源碼,涉及的比較深。而且并沒(méi)講 SpringBoot 下 mybatis 下的緩存知識(shí)點(diǎn),遂作此篇,以作補(bǔ)充。
緩存的配置
SpringBoot + mybatis 環(huán)境搭建很簡(jiǎn)單而且網(wǎng)上一堆教程,這里不班門(mén)弄斧了,記得在項(xiàng)目中將 mytatis 的源碼下載下來(lái)即可。mybaits 一共有兩級(jí)緩存:一級(jí)緩存的配置 key 是 localCacheScope,而二級(jí)緩存的配置 key 是 cacheEnabled,從名字上可以得出以下信息:
- 一級(jí)緩存是本地或者說(shuō)局部緩存,它不能被關(guān)閉,只能配置緩存范圍。SESSION 或者 STATEMENT。
- 二級(jí)緩存才是 mybatis 的正統(tǒng),功能會(huì)更強(qiáng)大些。
先來(lái)看下在 SpringBoot中 如何配置 mybatis 緩存的相關(guān)信息。默認(rèn)情況下 SpringBoot 下的 mybatis 一級(jí)緩存為 SESSION 級(jí)別,二級(jí)緩存也是打開(kāi)的,可以在 mybatis 源碼中的 org.apache.ibatis.session.Configuration.class 文件中看到(idea中打開(kāi)),如下圖:

也可以通過(guò)以下測(cè)試程序查看緩存開(kāi)啟情況:
@RunWith(SpringRunner.class)
@SpringBootTest
public class LearnApplicationTests {
private SqlSessionFactory factory;
@Before
public void setUp() throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
factory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void showDefaultCacheConfiguration() {
System.out.println("一級(jí)緩存范圍: " + factory.getConfiguration().getLocalCacheScope());
System.out.println("二級(jí)緩存是否被啟用: " + factory.getConfiguration().isCacheEnabled());
}
}
如果要設(shè)置一級(jí)緩存的緩存級(jí)別和開(kāi)關(guān)二級(jí)緩存,在 mybatis-config.xml (當(dāng)然也可以在 application.xml/yml 中配置)加入如下配置即可:
<settings> <setting name="cacheEnabled" value="true/false"/> <setting name="localCacheScope" value="SESSION/STATEMENT"/> </settings>
但需要注意的是二級(jí)緩存 cacheEnabled 只是個(gè)總開(kāi)關(guān),如果要讓二級(jí)緩存真正生效還需要在 mapper xml 文件中加入 。一級(jí)緩存只在同一 SESSION 或者 STATEMENT 之間共享,二級(jí)緩存可以跨 SESSION,開(kāi)啟后它們默認(rèn)具有如下特性:
- 映射文件中所有的 select 語(yǔ)句將被緩存
- 映射文件中所有的 insert/update/delete 語(yǔ)句將刷新緩存
一二級(jí)緩存同時(shí)開(kāi)啟的情況下,數(shù)據(jù)的查詢順序是 二級(jí)緩存 -> 一級(jí)緩存 -> 數(shù)據(jù)庫(kù)。一級(jí)緩存比較簡(jiǎn)單,而二級(jí)緩存可以設(shè)置更多的屬性,只需要在 mapper 的 xml 文件中的 中配置即可,具體如下:
<cache
type = "org.mybatis.caches.ehcache.LoggingEhcache" //指定使用的緩存類,mybatis默認(rèn)使用HashMap進(jìn)行緩存,可以指定第三方緩存
eviction = "LRU" //默認(rèn)是 LRU 淘汰緩存的算法,有如下幾種:
//1.LRU – 最近最少使用的:移除最長(zhǎng)時(shí)間不被使用的對(duì)象。
//2.FIFO – 先進(jìn)先出:按對(duì)象進(jìn)入緩存的順序來(lái)移除它們。
//3.SOFT – 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對(duì)象。
//4.WEAK – 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對(duì)象
flushInterval = "1000" //清空緩存的時(shí)間間隔,單位毫秒,可以被設(shè)置為任意的正整數(shù)。 默認(rèn)情況是不設(shè)置,也就是沒(méi)有刷新間隔,緩存僅僅調(diào)用語(yǔ)句時(shí)刷新。
size = "100" //緩存對(duì)象的個(gè)數(shù),任意正整數(shù),默認(rèn)值是1024。
readOnly = "true" //緩存是否只讀,提高讀取效率
blocking = "true" //是否使用阻塞緩存,默認(rèn)為false,當(dāng)指定為true時(shí)將采用BlockingCache進(jìn)行封裝,blocking,
//阻塞的意思,使用BlockingCache會(huì)在查詢緩存時(shí)鎖住對(duì)應(yīng)的Key,如果緩存命中了則會(huì)釋放對(duì)應(yīng)的鎖,
//否則會(huì)在查詢數(shù)據(jù)庫(kù)以后再釋放鎖這樣可以阻止并發(fā)情況下多個(gè)線程同時(shí)查詢數(shù)據(jù),詳情可參考BlockingCache的源碼。
/>
觸發(fā)緩存
配置一級(jí)緩存為 SESSION 級(jí)別
Controller 中調(diào)用兩次 getOne,代碼如下:
@RequestMapping("/getUser")
public UserEntity getUser(Long id) {
//第一次調(diào)用
UserEntity user1=userMapper.getOne(id);
//第二次調(diào)用
UserEntity user2=userMapper.getOne(id);
return user1;
}
調(diào)用: http://localhost:8080/getUser?id=1,打印結(jié)果如下:

從圖中的 1/2/3/4 可以看出每次 mapper 層的一次接口調(diào)用如 getOne 就會(huì)創(chuàng)建一個(gè) session,并且在執(zhí)行完畢后關(guān)閉 session。所以兩次調(diào)用并不在一個(gè) session 中,一級(jí)緩存并沒(méi)有發(fā)生作用。開(kāi)啟事務(wù),Controller 層代碼如下:
@RequestMapping("/getUser")
@Transactional(rollbackFor = Throwable.class)
public UserEntity getUser(Long id) {
//第一次調(diào)用
UserEntity user1=userMapper.getOne(id);
//第二次調(diào)用
UserEntity user2=userMapper.getOne(id);
return user1;
}
打印結(jié)果如下:

由于在同一個(gè)事務(wù)中,雖然調(diào)用了 select 操作兩次但是只執(zhí)行了一次 sql ,緩存發(fā)揮了作用。這就跟一開(kāi)始我遇到的那個(gè) BUG 場(chǎng)景一樣:同一 session 且 select 調(diào)用 > 1 次。如果在兩次調(diào)用中間插入 update 操作,緩存會(huì)立即失效。只要 session 中有 insert、update 和 delete 語(yǔ)句,該 session 中的緩存會(huì)立即被刷新。但是注意這只是在同一 session 之間。不同 session 之間如 session1 和 session2,session1 里的 insert/update/delete 并不會(huì)影響 session 2 下的緩存,這在高并發(fā)或者分布式的情況下會(huì)產(chǎn)生臟數(shù)據(jù)。所以建議將一級(jí)緩存級(jí)別調(diào)成 statement。
配置一級(jí)緩存為 STATEMENT 級(jí)別
再次將(1)中的無(wú)事務(wù)和有事務(wù)的代碼分別執(zhí)行一遍,打印結(jié)果始終如下:

配置成 SATEMENT 后,一級(jí)緩存相當(dāng)于被關(guān)閉了。STATEMENT 級(jí)別暫時(shí)不好模擬,但是我猜測(cè) STATEMENT 級(jí)別即在同一執(zhí)行 sql 的接口中(如上面的 getOne 中)緩存,出了 getOne 緩存即失效。
配置二級(jí)緩存,同時(shí)為了避免一級(jí)緩存的干擾,將一級(jí)緩存設(shè)置為 STATEMENT
Controller 中去掉 @Transactional 注解代碼如下:
@RequestMapping("/getUser")
public UserEntity getUser(Long id) {
UserEntity user1=userMapper.getOne(id);
UserEntity user2=userMapper.getOne(id);
return user1;
}
當(dāng)然二級(jí)緩存開(kāi)關(guān)保證打開(kāi),在 mapper xml 文件中加入 ,整個(gè)文件代碼如下:
<?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 namespace="com.binggle.learn.dao.mapper.UserMapper" >
<resultMap id="BaseResultMap" type="com.binggle.learn.dao.entity.UserEntity" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="sex" property="sex"/>
</resultMap>
<sql id="Base_Column_List" >
id, name, sex
</sql>
<select id="getOne" parameterType="java.lang.Long" resultMap="BaseResultMap" >
SELECT
<include refid="Base_Column_List" />
FROM users
WHERE id = #{id};
</select>
<cache />
</mapper>
執(zhí)行 http://localhost:8080/getUser?id=1,打印結(jié)果如下:

從圖中紅框可以看出第二次查詢命中緩存,0.5 是命中率。再次執(zhí)行 http://localhost:8080/getUser?id=1
打印結(jié)果如下:

這次一次 sql 也沒(méi)執(zhí)行了,緩存命中率上升到 0.75了,所以說(shuō)二級(jí)緩存全局緩存。但它的緩存范圍也是有限的,一級(jí)緩存在同一個(gè) session 中。二級(jí)緩存雖然可以跨 session 但也只能在同一 namespace 中,所謂 namespace 即 mapper xml 文件。具體實(shí)驗(yàn)請(qǐng)看《聊聊 mybatis 的緩存機(jī)制》中的關(guān)于二級(jí)緩存的實(shí)驗(yàn) 4 和 5。再看下二級(jí)緩存配置對(duì)二級(jí)緩存的影響,為了明顯的看出效果,只改如下配置:
<cache size="1" //一次只能緩存一個(gè)對(duì)象 flushInterval="5000" //刷新時(shí)間為 5s />
controller 代碼:
@RequestMapping("/getUser")
public UserEntity getUser(Long id, Long id2) {
//第一個(gè)對(duì)象 1
System.out.println("================緩存對(duì)象 1=================");
UserEntity user1 = userMapper.getOne(id);
//另一個(gè)對(duì)象 2
System.out.println("========緩存對(duì)象 2,剔除緩存中的對(duì)象 1=======");
UserEntity user2=userMapper.getOne(id2);
user2 = userMapper.getOne(id2);
//再次讀取第一個(gè)對(duì)象
System.out.println("==========緩存被剔除,執(zhí)行查詢 sql===========");
user1 = userMapper.getOne(id);
//暫停 5s
try {
sleep(5000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("============5s 后再次查詢對(duì)象 2=============");
user2 = userMapper.getOne(id2);
return user1;
}
執(zhí)行 http://localhost:8080/getUser?id=1&id2=2 最后打印的結(jié)果如下:

太長(zhǎng)了,拼接下:

可以看出二級(jí)緩存只能緩存一個(gè)對(duì)象且 5s 后就失效了,配置生效。緩存配置中還有一個(gè)重要的配置 type,該配置可以配置第三方的 cache,特別在高并發(fā)和分布式情況下。當(dāng)然,使用更專業(yè)的分布式緩存才是王道,例如 redis 等。
總結(jié)
本來(lái)想總結(jié)點(diǎn)什么的,但是覺(jué)得推薦文章中總結(jié)的非常好,直接引用了:
- MyBatis一級(jí)緩存的生命周期和SqlSession一致。
- MyBatis一級(jí)緩存內(nèi)部設(shè)計(jì)簡(jiǎn)單,只是一個(gè)沒(méi)有容量限定的HashMap,在緩存的功能性上有所欠缺。
- MyBatis的一級(jí)緩存最大范圍是SqlSession內(nèi)部,有多個(gè)SqlSession或者分布式的環(huán)境下,數(shù)據(jù)庫(kù)寫(xiě)操作會(huì)引起臟數(shù)據(jù),建議設(shè)定緩存級(jí)別為Statement。
- MyBatis的二級(jí)緩存相對(duì)于一級(jí)緩存來(lái)說(shuō),實(shí)現(xiàn)了SqlSession之間緩存數(shù)據(jù)的共享,同時(shí)粒度更加的細(xì),能夠到namespace級(jí)別,通過(guò)Cache接口實(shí)現(xiàn)類不同的組合,對(duì)Cache的可控性也更強(qiáng)。
- MyBatis在多表查詢時(shí),極大可能會(huì)出現(xiàn)臟數(shù)據(jù),有設(shè)計(jì)上的缺陷,安全使用二級(jí)緩存的條件比較苛刻。
- 在分布式環(huán)境下,由于默認(rèn)的MyBatis Cache實(shí)現(xiàn)都是基于本地的,分布式環(huán)境下必然會(huì)出現(xiàn)讀取到臟數(shù)據(jù),需要使用集中式緩存將MyBatis的Cache接口實(shí)現(xiàn),有一定的開(kāi)發(fā)成本,直接使用Redis、Memcached等分布式緩存可能成本更低,安全性也更高。
- 個(gè)人建議MyBatis緩存特性在生產(chǎn)環(huán)境中進(jìn)行關(guān)閉,單純作為一個(gè)ORM框架使用可能更為合適。
參考
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java二維數(shù)組與動(dòng)態(tài)數(shù)組ArrayList類詳解
這篇文章主要給大家介紹了關(guān)于Java二維數(shù)組與動(dòng)態(tài)數(shù)組ArrayList類的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
java中servlet實(shí)現(xiàn)登錄驗(yàn)證的方法
做web開(kāi)發(fā),登錄驗(yàn)證是免不了的,今天學(xué)習(xí)了servlet的登錄驗(yàn)證,當(dāng)然是很簡(jiǎn)單的,沒(méi)有使用session,request等作用域?qū)ο?,所以還是可以直接通過(guò)地址訪問(wèn)網(wǎng)頁(yè)的。2013-05-05
SpringBoot整合RabbitMQ 手動(dòng)應(yīng)答(簡(jiǎn)單demo)
這篇文章主要介紹了SpringBoot整合RabbitMQ 手動(dòng)應(yīng)答 簡(jiǎn)單demo,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01
使用Feign遠(yuǎn)程調(diào)用時(shí),序列化對(duì)象失敗的解決
這篇文章主要介紹了使用Feign遠(yuǎn)程調(diào)用時(shí),序列化對(duì)象失敗的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
Mybatis中foreach標(biāo)簽帶來(lái)的空格\換行\(zhòng)回車問(wèn)題及解決方案
這篇文章主要介紹了解決Mybatis中foreach標(biāo)簽帶來(lái)的空格,換行,回車問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04
Springboot讀取templates文件html代碼實(shí)例
這篇文章主要介紹了Springboot讀取templates文件html代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04
springboot+mybatis配置clickhouse實(shí)現(xiàn)插入查詢功能
這篇文章主要介紹了springboot+mybatis配置clickhouse實(shí)現(xiàn)插入查詢功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08

