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基本的使用学习结束。