背景
說起 mybatis,作為 Java 程式設計師應該是無人不知,它是常用的資料庫訪問框架。與 Spring 和 Struts 組成了 Java Web 開發的三劍客--- SSM。當然隨著 Spring Boot 的發展,現在越來越多的企業採用的是 SpringBoot + mybatis 的模式開發,我們公司也不例外。而 mybatis 對於我也僅僅停留在會用而已,沒想過怎麼去了解它,更不知道它的快取機制了,直到那個生死難忘的 BUG。故事的背景比較長,但並不是囉嗦,只是讓讀者知道這個 BUG 觸發的場景,加深記憶。在遇到類似問題時,可以迅速定位。
先說下故事的前提,為了防止使用者在動態中輸入特殊字元,使用者的動態都是編碼後發到後臺,而後臺在存入到 DB 表之前會解碼以方便在 DB 中檢視以及上報到搜尋引擎。在查詢使用者動態的時候先從 DB 表中讀取並在後臺做一次編碼再傳到前端,前端再解碼就可以正常展示了。流程如下圖:

- 整個操作過程都在一個函式中,而函式上面加了 @Transactional 的註解(對 mybatis 來說是在同一個 SESSION 中)
- 一般只會呼叫 findByIdy 一次,如果進入分支則會呼叫兩次 (第一次呼叫後做了編碼後被快取,第二次從快取讀後繼續被編碼)
便開始谷歌 mybatis 的快取機制,搜到了一篇非常不錯的文章《聊聊 mybatis 的快取機制》,推薦大家看一下。但是這篇文章講到了原始碼,涉及的比較深。而且並沒講 SpringBoot 下 mybatis 下的快取知識點,遂作此篇,以作補充。
快取的配置
SpringBoot + mybatis 環境搭建很簡單而且網上一堆教程,這裡不班門弄斧了,記得在專案中將 mytatis 的原始碼下載下來即可。mybaits 一共有兩級快取:一級快取的配置 key 是 localCacheScope,而二級快取的配置 key 是 cacheEnabled,從名字上可以得出以下資訊:
-
一級快取是本地或者說區域性快取,它不能被關閉,只能配置快取範圍。SESSION 或者 STATEMENT。
-
二級快取才是 mybatis 的正統,功能會更強大些。
先來看下在 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 中配置)加入如下配置即可:
<settings>
<setting name="cacheEnabled" value="true/false"/>
<setting name="localCacheScope" value="SESSION/STATEMENT"/>
</settings>
複製程式碼
但需要注意的是二級快取 cacheEnabled 只是個總開關,如果要讓二級快取真正生效還需要在 mapper xml 檔案中加入 。一級快取只在同一 SESSION 或者 STATEMENT 之間共享,二級快取可以跨 SESSION,開啟後它們預設具有如下特性:
- 對映檔案中所有的 select 語句將被快取
- 對映檔案中所有的 insert/update/delete 語句將重新整理快取
一二級快取同時開啟的情況下,資料的查詢順序是 二級快取 -> 一級快取 -> 資料庫。一級快取比較簡單,而二級快取可以設定更多的屬性,只需要在 mapper 的 xml 檔案中的 中配置即可,具體如下:
<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的原始碼。
/>
複製程式碼
觸發快取
- 配置一級快取為 SESSION 級別
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;
}
複製程式碼
列印結果如下:

- 配置一級快取為 STATEMENT 級別
再次將(1)中的無事務和有事務的程式碼分別執行一遍,列印結果始終如下:

- 配置二級快取,同時為了避免一級快取的干擾,將一級快取設定為 STATEMENT
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 的學習之路。
