缓存
- 什么是缓存:定义为 “存储在内存当中的数据”,作用是把用户常用数据放内存,让查询绕开硬盘 / 数据库文件,直接从内存(缓存)调,提升查询效率,缓解开发系统的性能问题 。
- 为什么使用缓存:优势是减少和数据库交互次数,降低系统开销,进而提升系统整体效率 。
- 什么样的数据能使用缓存:这部分内容显示不全,无法准确解释,若完整,一般会讲像高频访问、相对静态(更新不频繁)等特性的数据,适合用缓存来优化查询 ,比如网站首页固定配置信息、热门商品基础数据等 。 整体是对缓存概念、价值及适用数据的初步科普 。
一级缓存(Local Cache)
- 作用范围:SqlSession 级别,即同一个 SqlSession 内,执行相同且参数一致的 SQL 查询,会优先从缓存取结果,无需再查数据库。比如在一个业务方法里,多次查询同一用户信息,若用同一 SqlSession,后续查询就走一级缓存 。
- 默认状态:默认开启,且无法关闭 。
- 生命周期:与 SqlSession 一致,SqlSession 关闭,一级缓存随之清空 。
- 失效场景:SqlSession 期间执行 insert、update、delete 等写操作,缓存会被清空,保证数据一致性;查询条件不同(SQL 语句不同),也不会命中一级缓存 。在 Spring Boot 中,需开启
@Transactional
事务,让操作处于同一事务、同一 SqlSession ,一级缓存才更好生效 。
一个简单的列子。一个student表如下:
实体层
package com.qcby.entity;
public class Student {
private Integer id;
private String name;
private String gender;
private int age;
private int class_id;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", gender='" + gender + '\'' +
", age=" + age +
", class_id=" + class_id +
'}';
}
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 String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getClass_id() {
return class_id;
}
public void setClass_id(int class_id) {
this.class_id = class_id;
}
}
dao层
Student findById(Integer id);
mapper
<select id="findById" resultType="com.qcby.entity.Student">
SELECT * from students WHERE id=#{id}
</select>
测试层
@Test
public void findById (){
Student student1= studentDao.findById(1);
Student student2= studentDao.findById(1);
}
结果
结果可以看到sql语句仅进行了一次 ,与 MyBatis 的一级缓存有关。student1和student2是一个对象。
缓存失效的情况
1sqlSession不同
修改后的test代码,sqlSession1和sqlSession2两个session。
import com.qcby.Dao.StudentDao;
import com.qcby.entity.*;
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 org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class StudentTest {
private InputStream in = null;
private SqlSession session1 = null;
private SqlSession session2 = null;
private StudentDao studentDao1 = null;
private StudentDao studentDao2 = null;
@Before
public void init() throws IOException {
in = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
session1 = factory.openSession();
studentDao1 = session1.getMapper(StudentDao.class);
session2 = factory.openSession();
studentDao2 = session2.getMapper(StudentDao.class);
}
@After
public void destroy() throws IOException {
session1.close();
session2.close();
in.close();
}
/**
* session 不同
*/
@Test
public void findById (){
Student student1= studentDao1.findById(1);
Student student2= studentDao2.findById(1);
System.out.println(student1==student2);
}
}
sqlSession相同,条件不同
@Test
public void findById2 (){
Student student1= studentDao1.findById(1);
Student student2= studentDao1.findById(2);
System.out.println(student1==student2);
}
sqlSession相同,中间有操作
@Test
public void findById2 (){
Student student1= studentDao1.findById(1);
List<StudentCourse> courseRecords = Arrays.asList(
new StudentCourse(1, 1, 100),
new StudentCourse(1, 2, 1),
new StudentCourse(2, 1, 2),
new StudentCourse(2, 3, 3),
new StudentCourse(3, 2, 4)
);
int count = studentDao1.batchInsertCourseRecords(courseRecords);
Student student2= studentDao1.findById(1);
System.out.println(student1==student2);
}
sqlSession相同,手动清除一级缓存
@Test
public void findById2 (){
Student student1= studentDao1.findById(1);
session1.clearCache();
Student student2= studentDao1.findById(1);
System.out.println(student1==student2);
}
二级缓存(Global Cache)
- 作用范围:Mapper 映射器(namespace)级别,同一个 Mapper 的 namespace 下,不同 SqlSession 执行相同且参数一致的 SQL 查询,可共享二级缓存结果 。例如不同请求操作用户模块 Mapper(同一 namespace),查询相同用户数据能用到二级缓存 。
- 默认状态:默认关闭,需手动配置开启 。
- 缓存共享:跨 SqlSession,不同 SqlSession 实例可共享同一二级缓存 。
- 生效条件:要启用,需先在 MyBatis 全局配置里开启二级缓存(
cacheEnabled
设为true
,默认就是true
,但还需后续配置),再在具体 Mapper.xml 文件中通过<cache/>
标签配置,代表该 namespace 开启二级缓存 。且使用二级缓存的实体类需实现Serializable
序列化接口,方便缓存数据存储和传输 。 - 缓存同步:当二级缓存作用域(namespace )执行 C/U/D 操作,该作用域下所有 select 缓存会被清除,保证数据准确性 。
开启二级缓存
开启条件
1. 全局配置中启用二级缓存(核心配置文件)
<settings>
<setting name="cacheEnabled" value="true"/> <!-- 必须为true -->
</settings>
- 关键点:
cacheEnabled
必须设置为true
(你写的 "tnue" 可能是拼写错误) - 默认值:MyBatis 3.x 中默认值为
true
,但建议显式配置
2. 在映射文件中声明缓存(XML 方式)
<!-- 在 StudentMapper.xml 中 -->
<mapper namespace="com.qcby.mapper.StudentMapper">
<cache/> <!-- 声明启用二级缓存 -->
<!-- 或使用更详细的配置 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
</mapper>
- 注解方式:也可以在 Mapper 接口上使用
@CacheNamespace
注解 - 命名空间隔离:每个 Mapper 的缓存独立,除非使用
<cache-ref>
具体细节
(1). 回收策略(Eviction)
MyBatis 提供四种内置策略,对应不同的内存管理方式:
<!-- LRU(默认):最常用,适合热点数据 -->
<cache eviction="LRU"/>
<!-- FIFO:按进入顺序淘汰,适合批量处理场景 -->
<cache eviction="FIFO"/>
<!-- SOFT:基于 JVM 内存情况淘汰,内存不足时才回收 -->
<cache eviction="SOFT"/>
<!-- WEAK:更激进的回收策略,GC 时立即回收 -->
<cache eviction="WEAK"/>
适用场景:
- LRU:热点数据(如用户信息、配置项)
- FIFO:批量导入 / 导出时临时缓存
- SOFT/WEAK:大对象缓存(如文件内容),需谨慎使用
(2). 刷新间隔(Flush Interval)
控制缓存自动刷新的频率:
<!-- 每 60 秒自动清空缓存 -->
<cache flushInterval="60000"/>
<!-- 不设置(默认):仅在执行 CUD 操作时刷新 -->
<cache/>
注意事项:
- 设置过短会导致频繁重建缓存,影响性能
- 推荐在数据更新频率固定的场景使用(如定时任务)
(3). 缓存大小(Size)
控制缓存中可存储的最大对象数量:
<!-- 最多缓存 512 个对象 -->
<cache size="512"/>
性能建议:
- 过小会导致频繁淘汰,降低缓存命中率
- 过大会占用过多内存,需结合 JVM 堆大小调整
- 建议通过监控工具(如 VisualVM)分析缓存命中率
(4). 只读模式(Read Only)
决定缓存返回的是原始对象还是拷贝:
<!-- 性能优先:所有调用者共享同一实例(不可修改) -->
<cache readOnly="true"/>
<!-- 安全优先:每次返回对象的深拷贝(通过序列化) -->
<cache readOnly="false"/> <!-- 默认值 -->
3. 实体类必须实现 Serializable 接口
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
// 类定义
}
- 原因:二级缓存需要将对象序列化到本地或远程存储
- 注意事项:所有参与缓存的实体类都必须实现该接口
4. 二级缓存生效时机
try (SqlSession session = sqlSessionFactory.openSession()) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
Student student1 = mapper.findById(1); // 第一次查询,写入缓存
// 同一个 session 内,一级缓存优先
Student student2 = mapper.findById(1); // 命中一级缓存
// 关闭 session 后,一级缓存数据才会写入二级缓存
} // session.close() 触发缓存写入
// 新 session 查询相同数据
try (SqlSession newSession = sqlSessionFactory.openSession()) {
StudentMapper newMapper = newSession.getMapper(StudentMapper.class);
Student student3 = newMapper.findById(1); // 命中二级缓存
}
- 关键点:二级缓存的生效需要当前 SqlSession 关闭或提交
- 原理:一级缓存(SqlSession 级别)在会话结束时,将数据刷新到二级缓存(Mapper 级别)
二者对比总结
对比项 | 一级缓存 | 二级缓存 |
---|---|---|
作用域 | SqlSession 级别 | Mapper(namespace )级别 |
默认状态 | 默认开启,无法关闭 | 默认关闭,需手动配置开启 |
缓存共享范围 | 仅同一 SqlSession 内共享 | 同一 namespace 下跨 SqlSession 共享 |
数据同步时机 | SqlSession 内执行写操作清空 | namespace 内执行写操作清空 |
配置复杂度 | 无需额外配置 | 需全局开启 + Mapper 配置 + 实体类序列化 |