Mybatis入門-07-快取
一、前言
關於快取,官方文件有一些提及,不是很詳細,但足夠入門。
版本相關:
- MySQL 8.0.19
- MyBatis 3.5.5,注意:本內容需要開啟日誌
參考視訊:【狂神說Java】Mybatis最新完整教程IDEA版
二、簡介
2.1 快取
-
什麼是快取[Cache]?
- 存在記憶體中的臨時資料
- 將使用者基礎查詢的資料放在記憶體中,使用者去查詢資料就不用從硬碟上(關係型資料庫檔案)查詢,從快取中查詢,從而提高效率,用於解決高併發系統的效能問題
-
為什麼使用快取?
- 減少和資料庫的互動次數,減少系統開銷,提高系統效率
-
什麼樣的資料能使用快取?
- 經常查詢且不經常改變的資料
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 個引用。
- 快取會被視為讀/寫快取,這意味著獲取到的物件並不是共享的,可以安全地被呼叫者修改,而不干擾其他呼叫者或執行緒所做的潛在修改。
也就是說:
- 查詢不同的東西、不同的Mapper.xml,且之前沒查詢過,不會用到快取
- 增刪改操作,可能會改變原來的資料庫,所以必定會重新整理快取
- 手動清理快取
在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/>
開啟快取:
-
開啟全域性快取。
在配置檔案中(本文中的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基本的使用學習結束。