MyBatis筆記(五)——一級緩存,二級緩存

MyBatis筆記(五)——一級緩存,二級緩存


參考: https://www.bilibili.com/video/BV1NE411Q7Nx

視頻原作者CSDN: https://blog.csdn.net/qq_33369905

這篇是自己整理了一下,以便自己複習。

1.前期準備

1.1 緩存簡介

摘自 狂神說MyBatis07:緩存

1、什麼是緩存 [ Cache ]?

  • 存在內存中的臨時數據。

2、爲什麼使用緩存?

  • 減少和數據庫的交互次數,減少系統開銷,提高系統效率。

3、什麼樣的數據能使用緩存?

  • 經常查詢並且不經常改變的數據。

合理利用緩存可提高系統運行效率!

1.2 MyBatis中的自帶緩存簡介

MyBatis系統中默認定義了兩級緩存:一級緩存二級緩存

  • 一級緩存:默認開啓 SqlSession(對一個會話中的數據進行緩存)
  • 二級緩存:手動開啓和配置,基於某個xml映射文件的緩存

此外提供了緩存接口Cache,可自定義二級緩存。

1.3 數據表與實體類準備

-- ----------------------------
-- Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(20) NOT NULL,
  `name` varchar(30) DEFAULT NULL,
  `pwd` varchar(30) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '張三', '1123');
INSERT INTO `user` VALUES ('2', '李四', 'ksqo');
INSERT INTO `user` VALUES ('3', '王五', 'wwww');

User類

public class User {

    private int id;  //id
    private String name;   //姓名
    private String pwd;   //密碼

    //推薦Lombok
    //構造,有參,無參
    //set/get
    //toString()
}

2.MyBatis中的一級緩存

2.1 一級緩存效果

添加log4j日誌以便查看執行的SQL。

此不贅述,可查看:MyBatis筆記(三)——ResultMap結果集映射,日誌,分頁的多種實現方式

UserMapper接口類:

public interface UserMapper {
    
    //查詢所有User
    List<User> selectUser();

    //按照id查詢User
    User selectUserById(int id);
}

UserMapper.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.piao.dao.UserMapper">

    <select id="selectUser" resultType="com.piao.User">
      select * from user;
    </select>

    <select id="selectUserById" parameterType="int" resultType="com.piao.User">
        select * from user where id = #{id};
    </select>
</mapper>

測試:

    @Test
    public void testSelectUserById() {
        System.out.println("測試按照id查詢用戶selectUserById()");

        List<User> users = mapper.selectUser();
        for (User user : users) {
            System.out.println(user);
        }
        List<User> users2 = mapper.selectUser();
        for (User user : users) {
            System.out.println(user);
        }
        //比較兩次 查詢所有 的結果集
        System.out.println(users == users2);

        User user = mapper.selectUserById(2);
        System.out.println(user);
        User user2 = mapper.selectUserById(2);
        System.out.println(user);
        //比較兩次 按照id查詢 的結果集
        System.out.println(user == user2);
    }

輸出:可看到第二次查詢所有用戶和第二次查詢id爲2的用戶沒有與數據庫交互,沒有執行SQL。

這便是MyBatis默認開啓的一級緩存的效果,對於單條查詢SQL語句的結果,sqlsession會保存該數據。

DEBUG [main] - ==>  Preparing: select * from user; 
DEBUG [main] - ==> Parameters: 
TRACE [main] - <==    Columns: id, name, pwd
TRACE [main] - <==        Row: 1, 張三, 1123
TRACE [main] - <==        Row: 2, 李四, ksqo
TRACE [main] - <==        Row: 3, 王五, wwww
DEBUG [main] - <==      Total: 3
User{id=1, name='張三', pwd='1123'}
User{id=2, name='李四', pwd='ksqo'}
User{id=3, name='王五', pwd='wwww'}
User{id=1, name='張三', pwd='1123'}
User{id=2, name='李四', pwd='ksqo'}
User{id=3, name='王五', pwd='wwww'}
true
DEBUG [main] - ==>  Preparing: select * from user where id = ?; 
DEBUG [main] - ==> Parameters: 2(Integer)
TRACE [main] - <==    Columns: id, name, pwd
TRACE [main] - <==        Row: 2, 李四, ksqo
DEBUG [main] - <==      Total: 1
User{id=2, name='李四', pwd='ksqo'}
User{id=2, name='李四', pwd='ksqo'}
true

2.2 MyBatis一級緩存失效的四種情況

MyBatis一級緩存失效結果就是MyBatis會與數據庫交互,刷新一級緩存。

  • sqlSession不同

每個sqlSession中保存的數據是不共享的,相互獨立。因此使用不同的sqlSession對象去獲取mapper來查詢一摸一樣的數據,仍然會與數據庫交互。

  • sqlSession相同,查詢條件不同

剛剛上述的運行結果正驗證了這一點,一個查詢所有,一個按照id=2查詢,分別執行了SQL,後面一級緩存便更新成了按照id=2查詢的結果。

  • sqlSession相同,兩次查詢之間執行了增刪改操作
    //測試按照id查詢用戶selectUserById()
    @Test
    public void testSelectUserById() {
        System.out.println("測試按照id查詢用戶selectUserById()");
        User user = mapper.selectUserById(2);
        System.out.println(user);

        User user3 = new User(5, "趙六", "2333");
        mapper.insertUser(user3);
        session.commit();//提交事務  原生jdbc則需要開啓事務執行sql後提交事務

        User user2 = mapper.selectUserById(2);
        System.out.println(user);
        //比較兩次 按照id查詢 的結果集
        System.out.println(user == user2);
    }

輸出,重新查詢了數據庫

此時緩存已失效,因爲增刪改可能對數據產生影響,造成數據庫與緩存不一致的可能。

DEBUG [main] - ==>  Preparing: select * from user where id = ?; 
DEBUG [main] - ==> Parameters: 2(Integer)
TRACE [main] - <==    Columns: id, name, pwd
TRACE [main] - <==        Row: 2, 李四, ksqo
DEBUG [main] - <==      Total: 1
User{id=2, name='李四', pwd='ksqo'}
DEBUG [main] - ==>  Preparing: insert into user(id,name,pwd) values (?,?,?); 
DEBUG [main] - ==> Parameters: 5(Integer), 趙六(String), 2333(String)
DEBUG [main] - <==    Updates: 1
DEBUG [main] - ==>  Preparing: select * from user where id = ?; 
DEBUG [main] - ==> Parameters: 2(Integer)
TRACE [main] - <==    Columns: id, name, pwd
TRACE [main] - <==        Row: 2, 李四, ksqo
DEBUG [main] - <==      Total: 1
User{id=2, name='李四', pwd='ksqo'}
false
  • sqlSession相同,手動清除一級緩存
session.clearCache();//手動清除緩存

2.3 一級緩存流程圖

圖源百度

3.MyBatis中的二級緩存

由上述可看到MyBatis中一級緩存的侷限性,條件太苛刻(很容易失效),性能提升十分有限。

MyBatis中的二級緩存(也稱全局緩存) 基於namespace級別的緩存,一個名稱空間,對應一個二級緩存(全局緩存);

3.1開啓二級緩存(簡要版)

要啓用全局的二級緩存,只需要在你的 SQL 映射文件(***Mapper.xml)中添加:

    <!--當前mapper.xml中使用二級緩存-->
    <cache/>

然後在MyBatis-config.xml中的settings添加全局開啓緩存

    <settings>
        <!-- 開啓緩存(默認開啓)-->
        <setting name="cacheEnabled" value="true"/>
    </settings>

官方文檔有對應的說明(其實默認就是開啓的):

注意這個設置是: 全局性地開啓或關閉所有映射器配置文件中已配置的任何緩存。

也就是說,沒有加cache元素的xml映射文件依舊是默認的一級緩存!

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-82uQBNa1-1589514753471)(D:\博客markdown文件\CSDN\應用框架\MyBatis\MyBatis筆記(五)]——緩存.assets\image-20200515104647231.png)

3.2 二級緩存效果與工作機制

這個簡單語句的效果如下:

  • 映射語句文件中的所有 select 語句的結果將會被緩存。
  • 映射語句文件中的所有 insert、update 和 delete 語句會刷新緩存。
  • 緩存會使用最近最少使用算法(LRU, Least Recently Used)算法來清除不需要的緩存。
  • 緩存不會定時進行刷新(也就是說,沒有刷新間隔)。
  • 緩存會保存列表或對象(無論查詢方法返回哪種)的 1024 個引用。
  • 緩存會被視爲讀/寫緩存,這意味着獲取到的對象並不是共享的,可以安全地被調用者修改,而不干擾其他調用者或線程所做的潛在修改。

工作機制:

  • 在一級緩存的基礎上如果會話關閉了,一級緩存的數據就會被保存到二級緩存中
  • 新的會話查詢信息可以從二級緩存裏獲取內容**(如果一級緩存沒有的話)**
  • 不同mapper之間的數據是獨立的**(即二級緩存的作用域是namespace級別,存在各自的map中)**

3.3 自定義設置二級緩存cache元素屬性

這些屬性可以通過 cache 元素的屬性來修改。比如:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

這個更高級的配置創建了一個 FIFO 緩存,每隔 60 秒刷新,最多可以存儲結果對象或列表的 512 個引用,而且返回的對象被認爲是隻讀的,因此對它們進行修改可能會在不同線程中的調用者產生衝突。

可用的清除策略有:

  • LRU – 最近最少使用:移除最長時間不被使用的對象。
  • FIFO – 先進先出:按對象進入緩存的順序來移除它們。
  • SOFT – 軟引用:基於垃圾回收器狀態和軟引用規則移除對象。
  • WEAK – 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除對象。

默認的清除策略是 LRU。

flushInterval(刷新間隔)屬性可以被設置爲任意的正整數,設置的值應該是一個以毫秒爲單位的合理時間量。 默認情況是不設置,也就是沒有刷新間隔,緩存僅僅會在調用語句時刷新。

size(引用數目)屬性可以被設置爲任意正整數,要注意欲緩存對象的大小和運行環境中可用的內存資源。默認值是 1024。

readOnly(只讀)屬性可以被設置爲 true 或 false。只讀的緩存會給所有調用者返回緩存對象的相同實例。 因此這些對象不能被修改。這就提供了可觀的性能提升。而可讀寫的緩存會(通過序列化)返回緩存對象的拷貝。 速度上會慢一些,但是更安全,因此默認值是 false。

提示 二級緩存是事務性的。這意味着,當 SqlSession 完成並提交時,或是完成並回滾,但沒有執行 flushCache=true 的 insert/delete/update 語句時,緩存會獲得更新。

3.4 具體的mapper映射xml文件編寫舉例

在MyBatis-config.xml中的settings添加全局開啓緩存(其實默認就是開啓的):

    <settings>
        <!-- 開啓緩存(默認開啓)-->
        <setting name="cacheEnabled" value="true"/>
    </settings>

UserMapper接口:

//操作數據庫的接口
public interface UserMapper {

    //查詢所有User
    List<User> selectUser();

    //按照id查詢User
    User selectUserById(int id);
}

UserMapeer.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">
<!--namespace綁定一個對應的Mapper接口-->
<mapper namespace="com.piao.dao.UserMapper">

    <!--當前mapper.xml中使用二級緩存
    eviction="FIFO" 清除策略選擇先進先出
    flushInterval="60000" 每隔60s刷新
    size="512" 最多可以存儲結果對象或列表的 512 個引用,
    readOnly="true" 返回的對象被認爲是隻讀的
    -->
    <cache eviction="FIFO"
            flushInterval="60000"
            size="512"
            readOnly="true"/>

    <!--    parameterType參數類型-->
    <!--    傳參方式: #{id}  id爲selectUserById方法中的int類型參數id selectUserById(int id);-->
    <select id="selectUserById" parameterType="int" resultType="com.piao.User">
        select * from user where id = #{id};
    </select>
</mapper>

測試

    //測試二級緩存
    @Test
    public void testSelectUserById() {
        SqlSession session = MybatisUtils.getSession();
        User5Mapper mapper = session.getMapper(User5Mapper.class);
        SqlSession session2 = MybatisUtils.getSession();
        User5Mapper mapper2 = session2.getMapper(User5Mapper.class);
        SqlSession session3 = MybatisUtils.getSession();
        User5Mapper mapper3 = session3.getMapper(User5Mapper.class);

        User user = mapper.selectUserById(2);
        System.out.println(user);
        session.close();//關閉第一個sqlsession 會將一級緩存提交到二級緩存

        User user2 = mapper2.selectUserById(2);
        System.out.println(user);
        //比較兩次 按照id查詢 的結果集
        System.out.println(user == user2);
        session2.close();

        User user3 = mapper3.selectUserById(2);
        System.out.println(user);
        //比較兩次 按照id查詢 的結果集
        System.out.println(user == user3);
        session3.close();
    }

測試輸出:可看到第一個SsqlSession關閉後將一級緩存提交到二級緩存,後面兩個sqlsession從二級緩存中拿到id=2的數據,三次查詢出的結果集User都是同一個。

同時可看到日式的輸出有Cache Hit Ratio 緩存命中率:

命中率=從緩存中讀取數據的次數/所有訪問數據次數(磁盤讀取次數+緩存讀取次數)

第一次是0,因爲二級緩存裏沒有數據。第二次爲0.5,因爲第二次是從緩存拿到的數據,第三次是0.66666,說明也是從緩存中拿到的數據。就是隻有第一次是從MySQL讀取(磁盤讀取),整個過程只執行一次SQL

DEBUG [main] - Cache Hit Ratio [com.piao.dao.User5Mapper]: 0.0
DEBUG [main] - ==>  Preparing: select * from user where id = ?; 
DEBUG [main] - ==> Parameters: 2(Integer)
TRACE [main] - <==    Columns: id, name, pwd
TRACE [main] - <==        Row: 2, 李四, ksqo
DEBUG [main] - <==      Total: 1
User{id=2, name='李四', pwd='ksqo'}
DEBUG [main] - Cache Hit Ratio [com.piao.dao.User5Mapper]: 0.5
User{id=2, name='李四', pwd='ksqo'}
true
DEBUG [main] - Cache Hit Ratio [com.piao.dao.User5Mapper]: 0.6666666666666666
User{id=2, name='李四', pwd='ksqo'}
true

結果也驗證說明二級緩存是跨sqlsession的,作用域是namespace級別!

3.5 二級緩存流程圖

圖源百度

先走一級緩存,(如果開啓二級緩存的話)再走二級緩存,(如果有第三方緩存)再走第三方緩存,沒有就走數據庫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章