Mybatis缓存

缓存

  • 什么是缓存:定义为 “存储在内存当中的数据”,作用是把用户常用数据放内存,让查询绕开硬盘 / 数据库文件,直接从内存(缓存)调,提升查询效率,缓解开发系统的性能问题 。
  • 为什么使用缓存:优势是减少和数据库交互次数,降低系统开销,进而提升系统整体效率 。
  • 什么样的数据能使用缓存:这部分内容显示不全,无法准确解释,若完整,一般会讲像高频访问、相对静态(更新不频繁)等特性的数据,适合用缓存来优化查询 ,比如网站首页固定配置信息、热门商品基础数据等 。 整体是对缓存概念、价值及适用数据的初步科普 。

一级缓存(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 配置 + 实体类序列化