1. 程式人生 > 實用技巧 >Mybatis入門-07-快取

Mybatis入門-07-快取

一、前言

關於快取,官方文件有一些提及,不是很詳細,但足夠入門。

版本相關:

  • MySQL 8.0.19
  • MyBatis 3.5.5,注意:本內容需要開啟日誌

參考視訊:【狂神說Java】Mybatis最新完整教程IDEA版

二、簡介

2.1 快取

  1. 什麼是快取[Cache]?

    • 存在記憶體中的臨時資料
    • 將使用者基礎查詢的資料放在記憶體中,使用者去查詢資料就不用從硬碟上(關係型資料庫檔案)查詢,從快取中查詢,從而提高效率,用於解決高併發系統的效能問題
  2. 為什麼使用快取?

    • 減少和資料庫的互動次數,減少系統開銷,提高系統效率
  3. 什麼樣的資料能使用快取?

    • 經常查詢且不經常改變的資料

2.2 MyBatis快取

  • MyBatis包含一個非常強大的快取特性

  • MyBatis定義了兩級快取:一級快取二級快取

    預設情況下,一級快取開啟,即SqlSession級別的快取,也成為本地快取

  • 二級快取需要手動開啟和配置,他是基於namespace級別的快取

  • 為了提高擴充套件性,MyBatis定義了快取介面Cache,我們可以通過Cache介面去自定義二級快取。

三、準備工作

3.1 資料庫

建表,之前我在mybatis這個資料庫裡建好了表,現在再拿出來:

create table if not exists `User`(
    `id` INT(20) not null primary key,
    `name` VARCHAR(20) default null,
    `pwd` varchar(20) default null
)ENGINE=INNODB default CHARSET = UTF8;

insert into `User`(`id`,`name`,`pwd`)
values (1,'admin','123456'),
       (2,'Jax','123456'),
       (3,'Jinx','123455'),
       (4,'Query','123456'),
       (5,'biubiu','123456');

3.2 配置檔案

關於我mybatis配置、日誌配置的檔案,請自行配置,下面僅供參考:

路徑:

程式碼

db.properties,一些mybatis-config.xml用到的連線資料庫的資訊:

driver = com.mysql.jdbc.Driver
url = jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC
username = root
password = qq123456

mybatis-config.xml

:

注意:

  • 使用了SLF4J+log4J,如果想使用的話請加入依賴:

            <!--使用slf4j 作為日誌門面-->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.25</version>
            </dependency>
            <!--使用 log4j2 的介面卡進行繫結-->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-slf4j-impl</artifactId>
                <version>2.12.1</version>
                <scope>test</scope>
            </dependency>
    
    
            <!--log4j2 日誌門面-->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-api</artifactId>
                <version>2.12.1</version>
            </dependency>
            <!--log4j2 日誌實現-->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-core</artifactId>
                <version>2.12.1</version>
            </dependency>
    
    
  • <package name="com.duzhuan.pojo"/>可以看到使用了別名,以後pojo包裡的實體類的名字會不加全限定名而在Mapper中出現

  • <mapper class="com.duzhuan.dao.UerMapper"/>,這是我註冊的mapper,請按需設定。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <properties resource="db.properties"></properties>

    <settings>
        <setting name="logImpl" value="SLF4J"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    
    <typeAliases>
        <package name="com.duzhuan.pojo"/>
    </typeAliases>
    

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper class="com.duzhuan.dao.UerMapper"/>
    </mappers>
</configuration>

log4j2.xml,日誌配置,具體可以看Mybatis入門-03-日誌工廠,注意:本內容需要開啟日誌,因此不想使用下面複雜的日誌也沒有配置過日誌的可以參照Mybatis入門-03-日誌工廠裡設定STDOUT_LOGGING

<?xml version="1.0" encoding="UTF-8"?>

<!--
    status="debug" 日誌框架本身的級別
    configuration還有個屬性是 monitorInterval = 5,自動載入配置檔案的最小間隔時間,單位是秒
-->
<configuration status="debug">

    <!--
        集中配置屬性進行管理,使用時通過:${}
    -->
    <properties>
        <property name="LOG_HOME">./logs</property>
    </properties>

    <!--日誌處理器-->
    <!--先定義所有的appender -->
    <appenders>
        <!--這個輸出控制檯的配置 -->
        <Console name="Console" target="SYSTEM_OUT">
            <!--             控制檯只輸出level及以上級別的資訊(onMatch),其他的直接拒絕(onMismatch) -->
            <ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY"/>
            <!--             這個都知道是輸出日誌的格式 -->
            <PatternLayout pattern="%d{yyyy.MM.dd 'at' HH:mm:ss z} [%-5level] %class{36} %L %M - %msg%xEx%n"/>
        </Console>

        <!--檔案會打印出所有資訊,這個log每次執行程式會自動清空,由append屬性決定,這個也挺有用的,適合臨時測試用 -->
        <!--append為TRUE表示訊息增加到指定檔案中,false表示訊息覆蓋指定的檔案內容,預設值是true -->
        <File name="log" fileName="${LOG_HOME}/mybatis-log.log" append="false">
            <PatternLayout pattern="%d{yyyy.MM.dd 'at' HH:mm:ss z} [%-5level] %class{36} %L %M - %msg%xEx%n"/>
        </File>

        <!--
            新增過濾器ThresholdFilter,可以有選擇的輸出某個級別以上的類別
            onMatch="ACCEPT" onMismatch="DENY"意思是匹配就接受,否則直接拒絕
        -->
        <File name="ERROR" fileName="${LOG_HOME}/mybatis-error.log">
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%d{yyyy.MM.dd 'at' HH:mm:ss z} [%-5level] %class{36} %L %M - %msg%xEx%n"/>
        </File>

        <!--
            使用隨機讀寫流的日誌檔案輸出appender,效能提高
        -->
        <RandomAccessFile name="accessFile" fileName="${LOG_HOME}/mybatis-access.log">
            <PatternLayout pattern="%d{yyyy.MM.dd 'at' HH:mm:ss z} [%-5level] %class{36} %L %M - %msg%xEx%n"/>
        </RandomAccessFile>

        <!--
            這個會打印出所有的資訊,每次大小超過size,
            則這size大小的日誌會自動存入按年份-月份建立的資料夾下面並進行壓縮,
            作為存檔
         -->
        <RollingFile name="RollingFile" fileName="${LOG_HOME}/mybatis-web.log"
                     filePattern="logs/$${date:yyyy-MM}/web-%d{MM-dd-yyyy}-%i.log.gz">
            <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} [%-5level] %class{36} %L %M - %msg%xEx%n"/>
            <SizeBasedTriggeringPolicy size="2MB"/>
        </RollingFile>
    </appenders>


    <!--然後定義logger,只有定義了logger並引入的appender,appender才會生效 -->
    <loggers>
        <!--使用rootLogger配置   日誌級別level="trace" -->
        <root level="trace">
            <!--制定日誌使用的處理器-->
            <appender-ref ref="log"/>
            <appender-ref ref="ERROR" />
            <appender-ref ref="Console"/>
            <appender-ref ref="accessFile"/>
            <appender-ref ref="RollingFile"/>
        </root>
    </loggers>
</configuration>

3.3 實體類

路徑

程式碼

package com.duzhuan.pojo;

/**
 * @Autord: HuangDekai
 * @Date: 2020/9/21 10:34
 * @Version: 1.0
 * @since: jdk11
 */
public class User {
    int id;
    String name;
    String password;

    public User() {
    }

    public User(int id, String name, String password) {
        this.id = id;
        this.name = name;
        this.password = password;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

3.4 工具類

注意,這次工具類和之前的工具類有些許不同:

路徑

程式碼

package com.duzhuan.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

/**
 * @Autord: HuangDekai
 * @Date: 2020/9/21 10:39
 * @Version: 1.0
 * @since: jdk11
 */
public class MybatisUtils {
    private static SqlSessionFactory sqlSessionFactory;

    static{
        try {
            String config = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(config);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static SqlSession getSqlSession(){
        return sqlSessionFactory.openSession(true);
    }
}

如果是使用IDEA的話,可以比較清晰地看出來return sqlSessionFactory.openSession(true);openSession()中加true的作用。

就是開啟事務自動提交。

四、一級快取

4.1 Mapper

路徑

程式碼

UserMapper:

package com.duzhuan.dao;

import com.duzhuan.pojo.User;

/**
 * @Autord: HuangDekai
 * @Date: 2020/9/21 10:47
 * @Version: 1.0
 * @since: jdk11
 */
public interface UserMapper {
    User getUserById(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.duzhuan.dao.UserMapper">

    <resultMap id="UserMap" type="User">
        <result property="password" column="pwd"/>
    </resultMap>

    <select id="getUserById" parameterType="int" resultMap="UserMap">
        select * from mybatis.user where `id` = #{id}
    </select>
</mapper>

4.2 測試樣例

路徑

程式碼

package com.duzhuan.dao;

import com.duzhuan.pojo.User;
import com.duzhuan.utils.MybatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

/**
 * @Autord: HuangDekai
 * @Date: 2020/9/21 12:07
 * @Version: 1.0
 * @since: jdk11
 */
public class UserMapperTest {
    @Test
    public void getUserByIdTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();

        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user1 = mapper.getUserById(1);
        System.out.println(user1);
        System.out.println("==========================================");
        User user2 = mapper.getUserById(2);
        System.out.println(user2);
        System.out.println("==========================================");
        User user1once = mapper.getUserById(1);
        System.out.println(user1once);

        sqlSession.close();
    }
}

查詢了兩次id為1的使用者。

4.3 結果

4.4 快取失效的情況

官方文件中列了幾個說明:

  • 對映語句檔案中的所有 select 語句的結果將會被快取。
  • 對映語句檔案中的所有 insert、update 和 delete 語句會重新整理快取。
  • 快取會使用最近最少使用演算法(LRU, Least Recently Used)演算法來清除不需要的快取。
  • 快取不會定時進行重新整理(也就是說,沒有重新整理間隔)。
  • 快取會儲存列表或物件(無論查詢方法返回哪種)的 1024 個引用。
  • 快取會被視為讀/寫快取,這意味著獲取到的物件並不是共享的,可以安全地被呼叫者修改,而不干擾其他呼叫者或執行緒所做的潛在修改。

也就是說:

  1. 查詢不同的東西、不同的Mapper.xml,且之前沒查詢過,不會用到快取
  2. 增刪改操作,可能會改變原來的資料庫,所以必定會重新整理快取
  3. 手動清理快取

4.3 結果中已經展示了1,那麼試一下第二點:

測試第二點:增刪改

UserMapper中新增方法:

int updateUser(User user);

在UserMapper.xml中新增標籤:

    <update id="updateUser" parameterType="User">
        update mybatis.user set `name` = #{name}, pwd = #{password} where id = #{id}
    </update>

測試樣例新增:

@Test
public void updateUserTest(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = new User(6,"Uzi","555555");
    User user1 = mapper.getUserById(1);
    System.out.println(user1);
    System.out.println("==========================================");
    mapper.updateUser(user);
    System.out.println("==========================================");
    User user1once = mapper.getUserById(1);
    System.out.println(user1once);

    sqlSession.close();
}

結果:

可以看到,查詢的是id=1的使用者,改的是id=6的使用者,但是快取依舊失效了,即快取重新整理了。

增刪改可能會改變原來的資料,所以會重新整理快取,這是為了保持資料庫的一致性

測試第三點:手動清理快取

新增測試樣例:

@Test
public void getUserByIdTestAndClearCache(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user1 = mapper.getUserById(1);
    System.out.println(user1);
    System.out.println("==========================================");

    //清理快取
    sqlSession.clearCache();

    System.out.println("==========================================");
    User user1once = mapper.getUserById(1);
    System.out.println(user1once);

    sqlSession.close();
}

結果:

一級快取預設是開啟的,只在一次SqlSession中有效,也就是拿到連線到關閉連線這個區間(因為每個使用者都會建立一個連線,所以一級快取只有在一個使用者不停地重新整理一個頁面有用)。

一級快取就是一個Map。

五、二級快取

  • 二級快取也叫全域性快取,一級快取的作用域太低了,所以誕生了二級快取
  • 基於namespace級別的快取,一個名稱空間,對應一個二級快取
  • 工作機制:
    • 一個會話查詢一條資料,這個資料就會被放在當前會話的一級快取中;
    • 如果當前會話關閉了,這個會話對應的一級快取就沒有了;但是我們想要的是,會話關閉了,一級快取中的資料被儲存到二級快取中;
    • 新的會話查詢資訊,就可以從二級快取中獲取內容;
    • 不同的mapper查出的資料會放在自己對應的快取(map)中。

官方文件中說:

預設情況下,只啟用了本地的會話快取,它僅僅對一個會話中的資料進行快取。 要啟用全域性的二級快取,只需要在你的 SQL 對映檔案中新增一行:

<cache/>

開啟快取:

  1. 開啟全域性快取。

    在配置檔案中(本文中的mybatis-config.xml)的<settings>裡有這麼一個設定,雖然是預設開啟的,但是一般為了可讀性,我們會顯式新增<setting name="cacheEnabled" value="true"/>

    設定名 描述 有效值 預設值
    cacheEnabled 全域性性地開啟或關閉所有對映器配置檔案中已配置的任何快取。 true | false true

2.在UerMapper.xml(要使用二級快取的Mapper)中新增<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 語句時,快取會獲得更新。

5.1 Mapper

在本文的測試上,使用:

<cache/>

還是使用:

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

都不會有太大差別,這裡使用第二種,將其新增到要啟用二級快取的UserMapper.xml裡。

5.2 測試樣例

在UserMapperTest中新增:

@Test
public void getUserByIdTestAndTestCache(){
    SqlSession sqlSession1 = MybatisUtils.getSqlSession();
    SqlSession sqlSession2 = MybatisUtils.getSqlSession();

    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);

    User userById1 = mapper1.getUserById(1);
    System.out.println(userById1);
    System.out.println("=================================================");
    User userById2 = mapper2.getUserById(1);
    System.out.println(userById2);

    sqlSession1.close();
    sqlSession2.close();
}

5.3 結果

先註釋掉<cache/>看看不使用二級快取的情況:

可以看到,不僅是兩條SQL,而且是兩個連線。

然後將註釋去掉,啟用二級快取:

依舊是兩個連線兩條SQL。

其實在前面引用的官方文件中就有提示:

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

即,當一個連線關閉了,二級快取才有作用。現在修改測試樣例:

@Test
public void getUserByIdTestAndTestCache(){
    SqlSession sqlSession1 = MybatisUtils.getSqlSession();
    SqlSession sqlSession2 = MybatisUtils.getSqlSession();

    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);

    User userById1 = mapper1.getUserById(1);
    System.out.println(userById1);
    System.out.println("=================================================");
    sqlSession1.close();
    
    User userById2 = mapper2.getUserById(1);
    System.out.println(userById2);

    sqlSession2.close();
}

注意觀察順序。

註釋掉<cache/>時執行程式:

取消註釋後執行程式:

同時在測試樣例里加入一句System.out.println(userById1 == userById2);

顯然,是相同的物件。

5.4 可能遇到的問題

報錯:

Caused by: java.io.NotSerializableException: com.duzhuan.pojo.User

解決方法:0

使User實現序列化:

public class User implements Serializable{
.......
}

5.5 小結

  • 只要開啟了二級快取,在同一個名稱空間(Mapper)下有效
  • 所有的資料都會先放在一級快取中
  • 只有當會話提交,或者關閉的時候,才會提交到二級快取中

至此,Mybatis基本的使用學習結束。