MyBatis面试高频:缓存机制详解,一级/二级缓存区别吃透

4 阅读10分钟

在MyBatis开发中,缓存机制是提升查询性能的核心手段,也是面试中高频考察的知识点。很多开发者只知道MyBatis有缓存,却分不清一级缓存和二级缓存的作用范围、配置方式及使用场景,面试时容易答不全面。本文将从核心定义、两级缓存的详细解析、代码示例、使用注意事项等方面,清晰拆解MyBatis缓存机制,帮你快速掌握核心考点,规避使用误区。

一、一句话吃透MyBatis缓存核心

MyBatis采用两级缓存机制,一级缓存是SqlSession级别,默认开启,仅同一会话内共享,更新操作会自动清空;二级缓存是Mapper级别,需手动开启,支持多SqlSession共享,依赖序列化实现,可配置淘汰策略,两者均通过装饰器模式实现,也可集成第三方缓存扩展。

二、详细解析:一级缓存与二级缓存的核心差异

1. 一级缓存(Local Cache,本地缓存)

一级缓存是MyBatis默认开启的缓存,无需任何额外配置,其作用范围仅限于当前SqlSession,是最基础、最常用的缓存方式。

核心特性

  • 作用范围:SqlSession级别,即同一个数据库会话(同一个SqlSession实例)内有效,不同SqlSession之间的缓存相互独立,无法共享。

  • 开启方式:默认开启,无需在配置文件中添加任何配置,创建SqlSession后自动生效。

  • 生命周期:与SqlSession绑定,SqlSession关闭、提交事务、回滚事务,或执行增删改操作时,一级缓存会自动清空,避免缓存数据与数据库数据不一致。

  • 共享性:仅对当前SqlSession可见,其他SqlSession无法访问当前SqlSession的缓存数据。

工作原理与代码示例

一级缓存的核心逻辑是:同一SqlSession内,多次执行相同的SQL语句(参数也完全一致),第一次会查询数据库,将查询结果存入一级缓存;后续查询会直接从缓存中获取结果,无需再次访问数据库,从而提升查询效率。当执行更新操作(INSERT、UPDATE、DELETE)时,MyBatis会自动清空当前SqlSession的一级缓存,确保缓存数据与数据库数据同步。

// 1. 获取SqlSessionFactory实例(省略配置加载过程)
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

// 2. 开启第一个SqlSession(会话1)
SqlSession sqlSession1 = sqlSessionFactory.openSession();
// 获取Mapper接口实例
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);

// 第一次查询:id=1,无缓存,访问数据库,结果存入一级缓存
User user1 = userMapper1.selectUserById(1);
System.out.println("第一次查询结果:" + user1);

// 第二次查询:id=1,与第一次SQL和参数完全一致,命中一级缓存,不访问数据库
User user2 = userMapper1.selectUserById(1);
System.out.println("第二次查询结果(命中一级缓存):" + user2);

// 执行更新操作:修改id=1的用户信息,自动清空当前SqlSession的一级缓存
user1.setAge(26);
userMapper1.updateUser(user1);
// 提交事务,也会触发一级缓存清空
sqlSession1.commit();

// 第三次查询:id=1,缓存已被清空,重新访问数据库
User user3 = userMapper1.selectUserById(1);
System.out.println("第三次查询结果(缓存失效):" + user3);

// 关闭SqlSession,一级缓存随之销毁
sqlSession1.close();

// 3. 开启第二个SqlSession(会话2)
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 第四次查询:id=1,属于新的SqlSession,无缓存,访问数据库
User user4 = userMapper2.selectUserById(1);
System.out.println("新会话查询结果:" + user4);
sqlSession2.close();

从示例可以看出,同一个SqlSession内的重复查询会命中缓存,而更新操作、事务提交或会话关闭后,缓存会失效;新的SqlSession无法复用之前会话的缓存,这就是一级缓存的SqlSession级别特性。

缓存失效场景总结

以下几种情况会导致一级缓存失效,需重点注意:

  1. 执行INSERT、UPDATE、DELETE任意一种更新操作,无论是否提交事务,都会清空当前SqlSession的一级缓存;

  2. 手动调用sqlSession.clearCache()方法,主动清空一级缓存;

  3. 提交事务(sqlSession.commit())或回滚事务(sqlSession.rollback());

  4. 关闭SqlSession(sqlSession.close()),一级缓存随会话销毁。

2. 二级缓存(Second Level Cache,全局缓存)

二级缓存是Mapper级别(也叫命名空间级别)的缓存,默认不开启,需要通过显式配置启用。它的作用范围比一级缓存更广,多个SqlSession可以共享同一个Mapper的缓存数据,进一步提升查询性能。

核心特性

  • 作用范围:Mapper级别,即同一个Mapper接口(同一个命名空间)下的所有SqlSession共享缓存,跨SqlSession有效。

  • 开启方式:需手动配置,分为全局开启和Mapper级开启两步,缺一不可。

  • 生命周期:与应用生命周期一致,只要应用不停止,二级缓存就会一直存在(除非被主动清除或配置了自动刷新)。

  • 共享性:多个SqlSession可以共享同一Mapper的缓存数据,无需重复查询数据库。

配置方式(核心步骤)

二级缓存的开启需要两步配置,先全局启用,再针对具体Mapper配置缓存,具体如下:

第一步:全局开启二级缓存

在MyBatis的核心配置文件(mybatis-config.xml)中,通过settings标签开启二级缓存,默认值为true,也可显式配置:

<?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>
        <setting name="cacheEnabled" value="true"/>
   </settings>
    
    <!-- 其他配置(数据源、Mapper扫描等)省略 -->
</configuration>
第二步:Mapper级配置缓存

在需要启用二级缓存的Mapper XML文件中,添加标签,即可为该Mapper开启二级缓存,同时可配置缓存参数:

<?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命名空间,二级缓存的作用范围就是该命名空间 -->
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 开启二级缓存,并配置参数 -->
    <cache 
        eviction="LRU"         <!-- 缓存淘汰策略默认LRU -->
        flushInterval="60000"  <!-- 自动刷新间隔,单位毫秒,60秒刷新一次 -->
        size="1024"            <!-- 缓存最大对象数量,默认1024 -->
        readOnly="false"       <!-- 是否只读,false表示可读写 -->
     />

    <!-- 查询语句,需设置useCache="true"(默认true,可省略) -->
    <select id="selectUserById" parameterType="Integer" resultType="com.example.entity.User" useCache="true">
        SELECT id, username, age, email FROM t_user WHERE id = #{id}
    </select><!-- 更新语句,需设置flushCache="true"(默认true,可省略),执行后清空当前Mapper的二级缓存 -->
    <update id="updateUser" parameterType="com.example.entity.User" flushCache="true">
        UPDATE t_user SET username = #{username}, age = #{age} WHERE id = #{id}
    </update>
</mapper>

缓存参数说明(核心):

  • eviction:缓存淘汰策略,当缓存达到最大容量时,删除过期或不常用的缓存数据,可选值:

  • LRU(默认):最近最少使用,删除长时间未被访问的缓存;

  • FIFO:先进先出,按缓存存入顺序删除最早的缓存;

  • SOFT:软引用,当内存不足时,删除软引用关联的缓存;

  • WEAK:弱引用,一旦垃圾回收机制触发,立即删除弱引用关联的缓存。

  • flushInterval:自动刷新缓存的时间间隔(毫秒),超过该时间,缓存会自动清空,默认无间隔(不自动刷新)。

  • size:缓存可存储的最大对象数量,默认1024,超过后按淘汰策略删除缓存。

工作原理与代码示例

二级缓存的工作逻辑是:当多个SqlSession访问同一个Mapper的相同SQL(参数一致)时,第一个SqlSession查询数据库后,会将结果存入该Mapper的二级缓存;后续其他SqlSession查询相同SQL时,会先检查二级缓存,若存在数据则直接返回,无需访问数据库。当执行该Mapper的更新操作时,会自动清空该Mapper的二级缓存,确保数据一致性。

// 1. 获取SqlSessionFactory实例(省略配置加载)
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

// 2. 开启第一个SqlSession(会话1)
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
// 第一次查询:id=2,无二级缓存,访问数据库,结果存入二级缓存和当前会话的一级缓存
User user1 = userMapper1.selectUserById(2);
System.out.println("会话1第一次查询结果:" + user1);
// 关闭会话1,一级缓存销毁,但二级缓存数据保留
sqlSession1.close();

// 3. 开启第二个SqlSession(会话2)
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 第二次查询:id=2,一级缓存(会话2)无数据,检查二级缓存,命中缓存,不访问数据库
User user2 = userMapper2.selectUserById(2);
System.out.println("会话2查询结果(命中二级缓存):" + user2);

// 执行更新操作:修改id=2的用户信息,清空当前Mapper的二级缓存
user2.setEmail("test@163.com");
userMapper2.updateUser(user2);
sqlSession2.commit();

// 第三次查询:id=2,二级缓存已被清空,访问数据库
User user3 = userMapper2.selectUserById(2);
System.out.println("会话2更新后查询结果(缓存失效):" + user3);
sqlSession2.close();

// 4. 开启第三个SqlSession(会话3)
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
// 第四次查询:id=2,二级缓存无数据,访问数据库
User user4 = userMapper3.selectUserById(2);
System.out.println("会话3查询结果:" + user4);
sqlSession3.close();

注意:二级缓存默认采用序列化方式存储缓存数据,因此缓存的实体类(如User)必须实现Serializable接口,否则会抛出序列化异常。

// 实体类实现Serializable接口,支持二级缓存序列化存储
public class User implements Serializable {
    private Integer id;
    private String username;
    private Integer age;
    private String email;

    //  getter、setter、toString方法省略
}

3. 两级缓存的执行优先级与流程

当MyBatis同时启用一级缓存和二级缓存时,查询数据会遵循固定的优先级顺序,确保缓存的高效利用,具体流程如下:

  1. 优先查询当前SqlSession的一级缓存,若命中,直接返回结果;

  2. 若一级缓存未命中,查询当前Mapper的二级缓存,若命中,返回结果,并将结果存入当前SqlSession的一级缓存;

  3. 若两级缓存均未命中,执行SQL查询数据库,将查询结果依次存入二级缓存和一级缓存,再返回结果。

三、两级缓存核心区别总结表

特性一级缓存二级缓存
作用范围SqlSession级别(单一会话)Mapper级别(跨会话共享)
开启方式默认开启,无需配置需全局+Mapper级双重配置
生命周期与SqlSession绑定,会话关闭即销毁与应用生命周期一致
数据存储直接存储对象,无需序列化需序列化存储,实体类需实现Serializable
缓存失效触发更新操作、会话关闭、事务提交/回滚当前Mapper的更新操作、自动刷新、手动清除

四、缓存使用注意事项

  1. 一级缓存无需手动配置,但需注意:同一SqlSession内的更新操作会清空缓存,避免缓存脏读;关闭会话后,缓存数据会丢失,无法跨会话复用。

  2. 二级缓存启用后,实体类必须实现Serializable接口,否则会抛出序列化异常;若不需要某条查询语句使用二级缓存,可设置useCache="false"。

  3. 二级缓存适合读多写少、数据实时性要求不高的场景(如配置表、静态数据);若数据实时性要求高(如用户信息表),不建议开启二级缓存,避免出现脏读。

  4. 执行更新操作(增删改)时,MyBatis会自动清空对应Mapper的二级缓存,无需手动处理;若需手动清空二级缓存,可调用sqlSession.clearCache()方法。

  5. 分布式环境中,默认的二级缓存是单机缓存,无法实现多节点共享,需集成Redis、Ehcache等第三方分布式缓存框架,替换默认缓存实现。

以上就是MyBatis缓存机制的全部核心内容,重点掌握一级缓存和二级缓存的作用范围、配置方式及使用场景,面试时只需抓住“SqlSession级别vsMapper级别”“默认开启vs手动配置”“无需序列化vs需序列化”这几个关键点,就能轻松应对相关问题,同时在实际开发中也能合理利用缓存提升系统性能。