提及 mybatis,做爲 Java 程序員應該是無人不知,它是經常使用的數據庫訪問框架。與 Spring 和 Struts 組成了 Java Web 開發的三劍客--- SSM。固然隨着 Spring Boot 的發展,如今愈來愈多的企業採用的是 SpringBoot + mybatis 的模式開發,咱們公司也不例外。而 mybatis 對於我也僅僅停留在會用而已,沒想過怎麼去了解它,更不知道它的緩存機制了,直到那個生死難忘的 BUG。故事的背景比較長,但並非囉嗦,只是讓讀者知道這個 BUG 觸發的場景,加深記憶。在遇到相似問題時,能夠迅速定位。php
先說下故事的前提,爲了防止用戶在動態中輸入特殊字符,用戶的動態都是編碼後發到後臺,然後臺在存入到 DB 表以前會解碼以方便在 DB 中查看以及上報到搜索引擎。在查詢用戶動態的時候先從 DB 表中讀取並在後臺作一次編碼再傳到前端,前端再解碼就能夠正常展現了。流程以下圖: html
便開始谷歌 mybatis 的緩存機制,搜到了一篇很是不錯的文章《聊聊 mybatis 的緩存機制》,推薦你們看一下。可是這篇文章講到了源碼,涉及的比較深。並且並沒講 SpringBoot 下 mybatis 下的緩存知識點,遂做此篇,以做補充。前端
SpringBoot + mybatis 環境搭建很簡單並且網上一堆教程,這裏不班門弄斧了,記得在項目中將 mytatis 的源碼下載下來便可。mybaits 一共有兩級緩存:一級緩存的配置 key 是 localCacheScope,而二級緩存的配置 key 是 cacheEnabled,從名字上能夠得出如下信息:java
一級緩存是本地或者說局部緩存,它不能被關閉,只能配置緩存範圍。SESSION 或者 STATEMENT。程序員
二級緩存纔是 mybatis 的正統,功能會更強大些。redis
先來看下在 SpringBoot中 如何配置 mybatis 緩存的相關信息。默認狀況下 SpringBoot 下的 mybatis 一級緩存爲 SESSION 級別,二級緩存也是打開的,能夠在 mybatis 源碼中的 org.apache.ibatis.session.Configuration.class 文件中看到(idea中打開),以下圖:算法
@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("一級緩存範圍: " + factory.getConfiguration().getLocalCacheScope());
System.out.println("二級緩存是否被啓用: " + factory.getConfiguration().isCacheEnabled());
}
}
複製代碼
若是要設置一級緩存的緩存級別和開關二級緩存,在 mybatis-config.xml (固然也能夠在 application.xml/yml 中配置)加入以下配置便可:sql
<settings>
<setting name="cacheEnabled" value="true/false"/>
<setting name="localCacheScope" value="SESSION/STATEMENT"/>
</settings>
複製代碼
但須要注意的是二級緩存 cacheEnabled 只是個總開關,若是要讓二級緩存真正生效還須要在 mapper xml 文件中加入 。一級緩存只在同一 SESSION 或者 STATEMENT 之間共享,二級緩存能夠跨 SESSION,開啓後它們默認具備以下特性:數據庫
一二級緩存同時開啓的狀況下,數據的查詢順序是 二級緩存 -> 一級緩存 -> 數據庫。一級緩存比較簡單,而二級緩存能夠設置更多的屬性,只須要在 mapper 的 xml 文件中的 中配置便可,具體以下:apache
<cache type = "org.mybatis.caches.ehcache.LoggingEhcache" //指定使用的緩存類,mybatis默認使用HashMap進行緩存,能夠指定第三方緩存 eviction = "LRU" //默認是 LRU 淘汰緩存的算法,有以下幾種: //1.LRU – 最近最少使用的:移除最長時間不被使用的對象。 //2.FIFO – 先進先出:按對象進入緩存的順序來移除它們。 //3.SOFT – 軟引用:移除基於垃圾回收器狀態和軟引用規則的對象。 //4.WEAK – 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的對象 flushInterval = "1000" //清空緩存的時間間隔,單位毫秒,能夠被設置爲任意的正整數。 默認狀況是不設置,也就是沒有刷新間隔,緩存僅僅調用語句時刷新。 size = "100" //緩存對象的個數,任意正整數,默認值是1024。 readOnly = "true" //緩存是否只讀,提升讀取效率 blocking = "true" //是否使用阻塞緩存,默認爲false,當指定爲true時將採用BlockingCache進行封裝,blocking, //阻塞的意思,使用BlockingCache會在查詢緩存時鎖住對應的Key,若是緩存命中了則會釋放對應的鎖, //不然會在查詢數據庫之後再釋放鎖這樣能夠阻止併發狀況下多個線程同時查詢數據,詳情可參考BlockingCache的源碼。 />
複製代碼
Controller 中調用兩次 getOne,代碼以下:
@RequestMapping("/getUser")
public UserEntity getUser(Long id) {
//第一次調用
UserEntity user1=userMapper.getOne(id);
//第二次調用
UserEntity user2=userMapper.getOne(id);
return user1;
}
複製代碼
調用:http://localhost:8080/getUser?id=1,打印結果以下:
@RequestMapping("/getUser")
@Transactional(rollbackFor = Throwable.class)
public UserEntity getUser(Long id) {
//第一次調用
UserEntity user1=userMapper.getOne(id);
//第二次調用
UserEntity user2=userMapper.getOne(id);
return user1;
}
複製代碼
打印結果以下:
再次將(1)中的無事務和有事務的代碼分別執行一遍,打印結果始終以下:
Controller 中去掉 @Transactional 註解代碼以下:
@RequestMapping("/getUser")
public UserEntity getUser(Long id) {
UserEntity user1=userMapper.getOne(id);
UserEntity user2=userMapper.getOne(id);
return user1;
}
複製代碼
固然二級緩存開關保證打開,在 mapper xml 文件中加入 ,整個文件代碼以下:
<?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>
複製代碼
執行 http://localhost:8080/getUser?id=1,打印結果以下:
<cache size="1" //一次只能緩存一個對象 flushInterval="5000" //刷新時間爲 5s />
複製代碼
controller 代碼:
@RequestMapping("/getUser")
public UserEntity getUser(Long id, Long id2) {
//第一個對象 1
System.out.println("================緩存對象 1=================");
UserEntity user1 = userMapper.getOne(id);
//另外一個對象 2
System.out.println("========緩存對象 2,剔除緩存中的對象 1=======");
UserEntity user2=userMapper.getOne(id2);
user2 = userMapper.getOne(id2);
//再次讀取第一個對象
System.out.println("==========緩存被剔除,執行查詢 sql===========");
user1 = userMapper.getOne(id);
//暫停 5s
try {
sleep(5000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("============5s 後再次查詢對象 2=============");
user2 = userMapper.getOne(id2);
return user1;
}
複製代碼
執行 http://localhost:8080/getUser?id=1&id2=2 最後打印的結果以下:
原本想總結點什麼的,可是以爲推薦文章中總結的很是好,直接引用了:
- MyBatis一級緩存的生命週期和SqlSession一致。
- MyBatis一級緩存內部設計簡單,只是一個沒有容量限定的HashMap,在緩存的功能性上有所欠缺。
- MyBatis的一級緩存最大範圍是SqlSession內部,有多個SqlSession或者分佈式的環境下,數據庫寫操做會引發髒數據,建議設定緩存級別爲Statement。
- MyBatis的二級緩存相對於一級緩存來講,實現了SqlSession之間緩存數據的共享,同時粒度更加的細,可以到namespace級別,經過Cache接口實現類不一樣的組合,對Cache的可控性也更強。 5.MyBatis在多表查詢時,極大可能會出現髒數據,有設計上的缺陷,安全使用二級緩存的條件比較苛刻。
- 在分佈式環境下,因爲默認的MyBatis Cache實現都是基於本地的,分佈式環境下必然會出現讀取到髒數據,須要使用集中式緩存將MyBatis的Cache接口實現,有必定的開發成本,直接使用Redis、Memcached等分佈式緩存可能成本更低,安全性也更高。
- 我的建議MyBatis緩存特性在生產環境中進行關閉,單純做爲一個ORM框架使用可能更爲合適。
記得關注公衆號哦,記錄着一個 C++ 程序員轉 Java 的學習之路。