MyBatis 缓存
1. 什么是MyBatis缓存
上文介绍的 MyBatis 延迟加载,解决的是多表关联查询情况下的效率问题,但是对于单表查询,延迟加载没有作用,MyBatis 提供了缓存机制来解决单表查询情况下的效率问题。
使用缓存也是通过减少 Java 程序和数据库的交互次数来提高查询效率。比如,第一次查询出某个对象之后,MyBatis 会自动将它存入缓存,当下一次查询该对象时,可以直接从缓存中获取,而不必再次访问数据库。
如果执行删除、修改操作,MyBatis 会自动清空缓存,从而保证数据的时效性与正确性。
MyBatis 有两种缓存方式:一级缓存与二级缓存,二者的区别在于作用域不同。
2. 一级缓存
-
MyBatis 自带一级缓存,SqlSession 级别,默认开启,并且无法关闭。
-
MyBatis 的一级缓存是 SqlSession 级别的缓存,在操作数据库时需要构建 SqlSession 对象,在对象里有一个 HashMap 用于存储缓存数据,不同的 SqlSession 之间缓存数据(HashMap)是互不影响的。
-
一级缓存的作用域是 SqlSession 范围的,当在同一个 SqlSession 中执行两次相同的 sql 语句时,第一次执行完毕会将数据库中查询的数据写到缓存(内存)中,第二次查询时会从缓存中读取数据,而不再去底层进行数据库查询,从而提高了查询效率。
-
需要注意的是,如果 SqlSession 执行 DML操作(删除和更新),并执行 commit() 操作,则 MyBatis 会自动清空 SqlSession 中的一级缓存,这样做的目的是为了保证缓存数据的时效性和正确性,避免出现读脏现象。
-
当一个 SqlSession 结束后(
SqlSession.close()
),该 SqlSession 中的一级缓存也就不存在了,Mybatis 默认开启一级缓存,不需要进行任何配置。
以单表查询为例,介绍 MyBatis 缓存
1、创建实体类 User
package com.training.entity;
public class User {
private Integer id;
private String name;
private Double score;
public User(){
}
public User(Integer id, String name, Double score) {
this.id = id;
this.name = name;
this.score = score;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getScore() {
return score;
}
public void setScore(Double score) {
this.score = score;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", score=" + score +
'}';
}
}
2、自定义 UserMapper 接口
package com.training.mapper;
import com.training.entity.User;
public interface UserMapper {
public String findById(Integer id);
}
3、SQL 配置文件 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.training.mapper.UserMapper">
<select id="findById" parameterType="int" resultType="com.training.entity.User">
select * from user where id=#{id};
</select>
</mapper>
4、全局配置文件 config.xml
<?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>
<settings>
<!-- 打印SQL -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!--数据源-->
<environments default="dev">
<!--可以配置多个环境-->
<environment id="dev">
<!--配置JDBC事务管理-->
<transactionManager type="JDBC"></transactionManager>
<!-- POOLED配置JDBC数据库连接池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</dataSource>
</environment>
</environments>
<!--注册UserMapper.xml -->
<mappers>
<mapper resource="com/training/mapper/UserMapper.xml"></mapper>
</mappers>
</configuration>
5、编写 Test 测试类
public class Test {
public static void main(String[] args) {
//加载MyBatis配置信息
InputStream inputStream = Test.class.getClassLoader().getResourceAsStream("config.xml");
//构建SQLSessionFactoryBuilder
SqlSessionFactoryBuilder builder = new
SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
//获取SqlSession
SqlSession sqlSession = factory.openSession();
//获取实现接口的代理对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.findById(1);
System.out.println(user1);
User user2 = mapper.findById(1);
System.out.println(user2);
}
}
控制台打印的信息如下:
从结果中可以看出,Java 程序执行了两次查询操作,但是 MyBatis 底层仅执行了一次 SQL 查询。查询出两个对象,第一个对象是通过SQL查询的,并 保存到缓存中,第二个对象是直接从缓存中获取的。
public class Test {
public static void main(String[] args) {
//加载MyBatis配置信息
InputStream inputStream = Test.class.getClassLoader().getResourceAsStream("config.xml");
//构建SQLSessionFactoryBuilder
SqlSessionFactoryBuilder builder = new
SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
//获取SqlSession
SqlSession sqlSession1 = factory.openSession();
//获取实现接口的代理对象
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.findById(1);
System.out.println(user1);
SqlSession sqlSession2 = factory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.findById(1);
System.out.println(user2);
}
}
控制台打印的信息如下:
说明:一级缓存是 SqlSession 级别的缓存,在操作数据库时需要构建 SqlSession 对象,在对象里有一个 HashMap 用于存储缓存数据,不同的 SqlSession 之间缓存数据(HashMap)是互不影响的。
3. 二级缓存
一级缓存不需要进行任何配置,可以直接使用。当一级缓存失效时,可以开启二级缓存,同时实现数据共享的功能,二级缓存是比一级缓存作用域更大的缓存机制,是 Mapper 级别的,只要是同一个 Mapper,无论使用多少个 SqlSession 来创建,数据都是共享的,多个 SqlSession 可以共有二级缓存。
MyBatis 二级缓存默认是关闭的,需要使用的时候,可以通过手动设置来开启。
1、config.xml 配置文件中开启二级缓存
<settings>
<!-- 打印SQL -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!-- 开启二级缓存 -->
<setting name="catchEnabled" value="true"></setting>
</settings>
2、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.training.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache></cache>
<select id="findById" parameterType="int" resultType="com.training.entity.User">
select * from t_user where id=#{id};
</select>
</mapper>
3、实体类 User 实现 Serializable 接口
package com.training.entity;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private Integer id;
private String name;
private Double score;
}
4、测试二级缓存
public class Test {
public static void main(String[] args) {
//加载MyBatis配置信息
InputStream inputStream = Test.class.getClassLoader().getResourceAsStream("config.xml");
//构建SQLSessionFactoryBuilder
SqlSessionFactoryBuilder builder = new
SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
//获取SqlSession
SqlSession sqlSession1 = factory.openSession();
//获取实现接口的代理对象
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.findById(1);
System.out.println(user1);
//关闭sqlSession1或sqlSession1.commit(),将一级缓存的数据同步到二级缓存
sqlSession1.close();
SqlSession sqlSession2 = factory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.findById(1);
System.out.println(user2);
}
}
控制台输出的信息如下:
SqlSession 在没有提交的情况下,不会将数据同步到二级缓存中,所以会导致第二次查询仍然要执行 SQL,所以一般执行完毕之后需要关闭 SqlSession,关闭的同时会将一级缓存中的数据自动提交到二级缓存。
但是在实际项目开发中,使用的是 ehcache 二级缓存,使用方式与上面相似
Step1:pom.xml 添加相关依赖
<!--mybatis 启用ehcache 二级缓存-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.8.3</version>
</dependency>
Step2:添加 ehcache.xml(与 config.xml 全局配置文件同级)
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<diskStore/>
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
Step3:config.xml 配置开启二级缓存
<settings>
<!-- 打印SQL -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"></setting>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"></setting>
</settings>
Step4:UserMapper.xml 中配置二级缓存
<!-- 开启二级缓存 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
<!-- 缓存创建之后,最后⼀次访问缓存的时间⾄缓存失效的时间间隔 -->
<property name="timeToIdleSeconds" value="3600"/>
<!-- 缓存⾃创建时间起⾄失效的时间间隔 -->
<property name="timeToLiveSeconds" value="3600"/>
<!-- 缓存回收策略,LRU表示移除近期使⽤最少的对象 -->
<property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>
Step5:与上面介绍有所不同的是,实体类不需要实现序列化接口
package com.training.entity;
import lombok.Data;
@Data
public class User {
private Integer id;
private String name;
private Double score;
}
同样的测试代码,控制台的输出结果如下:
4. 总结
-
MyBatis 框架的缓存分为两种:一级缓存和二级缓存,一级缓存是 SqlSession 级别的,二级缓存是 Mapper 级别的,使用的时候需要注意两种缓存的区别,一级缓存是默认开启且无法关闭的,二级缓存需要手动开启。
-
缓存机制和延迟加载功能类似,都是通过减少 Java 程序和数据库的交互次数来提高系统的运行速度,缓存机制更多针对于单表查询,延迟加载更多针对多表关联查询。
-
缓存机制使用的前提是两次查询之间没有数据修改与删除的操作,如果有修改和删除的操作,那么缓存就会自动清空,从而保证数据的时效性与正确性。