SpringBoot 下 Mybatis 的緩存

背景

  提及 mybatis,做爲 Java 程序員應該是無人不知,它是經常使用的數據庫訪問框架。與 Spring 和 Struts 組成了 Java Web 開發的三劍客--- SSM。固然隨着 Spring Boot 的發展,如今愈來愈多的企業採用的是 SpringBoot + mybatis 的模式開發,咱們公司也不例外。而 mybatis 對於我也僅僅停留在會用而已,沒想過怎麼去了解它,更不知道它的緩存機制了,直到那個生死難忘的 BUG。故事的背景比較長,但並非囉嗦,只是讓讀者知道這個 BUG 觸發的場景,加深記憶。在遇到相似問題時,能夠迅速定位。php

  先說下故事的前提,爲了防止用戶在動態中輸入特殊字符,用戶的動態都是編碼後發到後臺,然後臺在存入到 DB 表以前會解碼以方便在 DB 中查看以及上報到搜索引擎。在查詢用戶動態的時候先從 DB 表中讀取並在後臺作一次編碼再傳到前端,前端再解碼就能夠正常展現了。流程以下圖: html

  有一天後端預發環境發佈完畢後,用戶的動態頁面有的動態顯示正常,而有的倒是被編碼過的。看到現象後的第一個反應就是有問題的動態被編碼了兩次,可是編碼操做只會在 service 層的 findById 中有。理論不會在上層犯這種低級錯誤。話很少說便開始排查新增長的代碼,發現只要進入了新增長代碼中的某個 if 分支則被編碼了兩次。分支中除了再次調用 findById(必要性不討論),也無其餘特殊代碼了。百思不得其解後請教了旁邊的老司機,老司機說多是 mybatis 緩存。因而看了下我代碼,將編碼的操做從 findById 中移出來後再次發佈到預發,正常了,心想老司機不愧是老司機。本次 BUG 觸發的有兩個條件須要注意:

  • 整個操做過程都在一個函數中,而函數上面加了 @Transactional 的註解(對 mybatis 來講是在同一個 SESSION 中)
  • 通常只會調用 findByIdy 一次,若是進入分支則會調用兩次 (第一次調用後作了編碼後被緩存,第二次從緩存讀後繼續被編碼)

  便開始谷歌 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,開啓後它們默認具備以下特性:數據庫

  • 映射文件中全部的 select 語句將被緩存
  • 映射文件中全部的 insert/update/delete 語句將刷新緩存

  一二級緩存同時開啓的狀況下,數據的查詢順序是 二級緩存 -> 一級緩存 -> 數據庫。一級緩存比較簡單,而二級緩存能夠設置更多的屬性,只須要在 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" //緩存對象的個數,任意正整數,默認值是1024readOnly = "true" //緩存是否只讀,提升讀取效率 blocking = "true" //是否使用阻塞緩存,默認爲false,當指定爲true時將採用BlockingCache進行封裝,blocking, //阻塞的意思,使用BlockingCache會在查詢緩存時鎖住對應的Key,若是緩存命中了則會釋放對應的鎖, //不然會在查詢數據庫之後再釋放鎖這樣能夠阻止併發狀況下多個線程同時查詢數據,詳情可參考BlockingCache的源碼。 />
複製代碼

觸發緩存

  1. 配置一級緩存爲 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,打印結果以下:

  從圖中的 1/2/3/4 能夠看出每次 mapper 層的一次接口調用如 getOne 就會建立一個 session,而且在執行完畢後關閉 session。因此兩次調用並不在一個 session 中,一級緩存並無發生做用。開啓事務,Controller 層代碼以下:

@RequestMapping("/getUser")
@Transactional(rollbackFor = Throwable.class)
public UserEntity getUser(Long id) {
    //第一次調用
    UserEntity user1=userMapper.getOne(id);
    //第二次調用
    UserEntity user2=userMapper.getOne(id);
    return user1;
}
複製代碼

  打印結果以下:

  因爲在同一個事務中,雖然調用了 select 操做兩次可是隻執行了一次 sql ,緩存發揮了做用。這就跟一開始我遇到的那個 BUG 場景同樣:同一 session 且 select 調用 > 1 次。若是在兩次調用中間插入 update 操做,緩存會當即失效。只要 session 中有 insert、update 和 delete 語句,該 session 中的緩存會當即被刷新。可是注意這只是在同一 session 之間。不一樣 session 之間如 session1 和 session2,session1 裏的 insert/update/delete 並不會影響 session 2 下的緩存,這在高併發或者分佈式的狀況下會產生髒數據。因此建議將一級緩存級別調成 statement。

  1. 配置一級緩存爲 STATEMENT 級別

  再次將(1)中的無事務和有事務的代碼分別執行一遍,打印結果始終以下:

  配置成 SATEMENT 後,一級緩存至關於被關閉了。STATEMENT 級別暫時很差模擬,可是我猜想 STATEMENT 級別即在同一執行 sql 的接口中(如上面的 getOne 中)緩存,出了 getOne 緩存即失效。

  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,打印結果以下:

  從圖中紅框能夠看出第二次查詢命中緩存,0.5 是命中率。再次執行 http://localhost:8080/getUser?id=1 打印結果以下:

  此次一次 sql 也沒執行了,緩存命中率上升到 0.75了,因此說二級緩存全局緩存。但它的緩存範圍也是有限的,一級緩存在同一個 session 中。二級緩存雖然能夠跨 session 但也只能在同一 namespace 中,所謂 namespace 即 mapper xml 文件。具體實驗請看《聊聊 mybatis 的緩存機制》中的關於二級緩存的實驗 4 和 5。再看下二級緩存配置對二級緩存的影響,爲了明顯的看出效果,只改以下配置:

<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 最後打印的結果以下:

  太長了,拼接下:
  能夠看出二級緩存只能緩存一個對象且 5s 後就失效了,配置生效。緩存配置中還有一個重要的配置 type,該配置能夠配置第三方的 cache,特別在高併發和分佈式狀況下。固然,使用更專業的分佈式緩存纔是王道,例如 redis 等。

總結

  原本想總結點什麼的,可是以爲推薦文章中總結的很是好,直接引用了:

  1. MyBatis一級緩存的生命週期和SqlSession一致。
  2. MyBatis一級緩存內部設計簡單,只是一個沒有容量限定的HashMap,在緩存的功能性上有所欠缺。
  3. MyBatis的一級緩存最大範圍是SqlSession內部,有多個SqlSession或者分佈式的環境下,數據庫寫操做會引發髒數據,建議設定緩存級別爲Statement。
  4. MyBatis的二級緩存相對於一級緩存來講,實現了SqlSession之間緩存數據的共享,同時粒度更加的細,可以到namespace級別,經過Cache接口實現類不一樣的組合,對Cache的可控性也更強。 5.MyBatis在多表查詢時,極大可能會出現髒數據,有設計上的缺陷,安全使用二級緩存的條件比較苛刻。
  5. 在分佈式環境下,因爲默認的MyBatis Cache實現都是基於本地的,分佈式環境下必然會出現讀取到髒數據,須要使用集中式緩存將MyBatis的Cache接口實現,有必定的開發成本,直接使用Redis、Memcached等分佈式緩存可能成本更低,安全性也更高。
  6. 我的建議MyBatis緩存特性在生產環境中進行關閉,單純做爲一個ORM框架使用可能更爲合適。

參考

聊聊MyBatis緩存機制

  記得關注公衆號哦,記錄着一個 C++ 程序員轉 Java 的學習之路。

相關文章
相關標籤/搜索