MyBatis 提供了两级缓存机制:一级缓存(Local Cache)二级缓存(Global Cache),用于提升数据库查询性能。合理使用缓存可以显著减少数据库访问次数,提升系统响应速度。


一、核心概念

1. 一级缓存(Local Cache / Session Cache)

  • 作用范围SqlSession 级别。
  • 生命周期:与 SqlSession 绑定,SqlSession 关闭或清空后缓存失效。
  • 默认开启:无需配置,默认开启。
  • 存储位置:内存中,属于当前 SqlSession 的本地缓存。
  • 命中条件
    • 同一个 SqlSession
    • 相同的 SQL 语句
    • 相同的参数
    • 相同的 RowBounds
    • 相同的 Statement ID

✅ 示例:同一个 SqlSession 中执行两次 selectById(1),第二次直接从缓存返回。


2. 二级缓存(Global Cache / Mapper Cache)

  • 作用范围namespace 级别(即一个 Mapper 接口对应一个缓存)。
  • 生命周期:跨 SqlSession,应用级缓存。
  • 默认关闭:需手动开启。
  • 存储位置:可配置为内存(如 PerpetualCache)、Redis、Ehcache 等。
  • 命中条件
    • 不同的 SqlSession
    • 相同的 namespace(Mapper)
    • 相同的 SQL 和参数
    • 数据未被 INSERTUPDATEDELETE 操作清空

✅ 示例:SqlSession1 查询 UserMapper.selectById(1)SqlSession2 再次查询,可从二级缓存命中。


二、操作步骤(非常详细)

步骤 1:准备环境

确保项目中已集成 MyBatis 或 MyBatis-Plus。

<!-- Maven 依赖示例 -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.13</version>
</dependency>

步骤 2:启用二级缓存(全局配置)

mybatis-config.xmlapplication.yml 中开启二级缓存。

方式一:XML 配置(mybatis-config.xml

<configuration>
    <settings>
        <!-- 开启二级缓存 -->
        <setting name="cacheEnabled" value="true"/>
    </settings>
</configuration>

方式二:Spring Boot 配置(application.yml

mybatis:
  configuration:
    cache-enabled: true

⚠️ 注意:cacheEnabled 默认为 true,但建议显式声明。


步骤 3:在 Mapper XML 中启用缓存

在需要缓存的 Mapper.xml 文件中添加 <cache /> 标签。

<?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.example.mapper.UserMapper">

    <!-- 启用二级缓存 -->
    <cache />

    <select id="selectById" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>

    <update id="update" parameterType="User">
        UPDATE user SET name = #{name} WHERE id = #{id}
    </update>

</mapper>

步骤 4:实体类实现 Serializable

二级缓存会序列化对象,因此实体类必须实现 Serializable

public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long id;
    private String name;
    private Integer age;
    // getter/setter
}

步骤 5:测试一级缓存

@Test
public void testFirstLevelCache() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    // 第一次查询,访问数据库
    User user1 = mapper.selectById(1L);
    System.out.println("第一次查询: " + user1);

    // 第二次查询,命中一级缓存(不访问数据库)
    User user2 = mapper.selectById(1L);
    System.out.println("第二次查询: " + user2);

    // 清空缓存或关闭 SqlSession 后缓存失效
    sqlSession.clearCache(); // 手动清空

    // 再次查询,重新访问数据库
    User user3 = mapper.selectById(1L);
    System.out.println("清空后查询: " + user3);

    sqlSession.close();
}

步骤 6:测试二级缓存

@Test
public void testSecondLevelCache() {
    // SqlSession1
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    User user1 = mapper1.selectById(1L);
    System.out.println("SqlSession1 查询: " + user1);
    sqlSession1.close(); // 关闭后,结果写入二级缓存

    // SqlSession2
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    User user2 = mapper2.selectById(1L);
    System.out.println("SqlSession2 查询: " + user2); // 应从二级缓存读取
    sqlSession2.close();
}

三、常见错误与解决方案

错误现象 原因 解决方案
二级缓存未生效 未添加 <cache/> 标签 Mapper.xml 中添加 <cache />
抛出 NotSerializableException 实体类未实现 Serializable 添加 implements Serializable
更新后缓存未刷新 缓存未自动清空 确保 INSERTUPDATEDELETE 会自动清空缓存
SqlSession 未共享缓存 多个 SqlSession 使用不同 namespace 确保 namespace 一致
缓存数据过期或丢失 缓存实现未配置过期策略 使用 Redis/Ehcache 并配置 TTL

四、注意事项

  1. 缓存清空时机

    • 任何 INSERTUPDATEDELETE 操作都会清空当前 namespace 的二级缓存
    • sqlSession.clearCache() 清空一级缓存。
  2. 缓存粒度

    • 二级缓存是 namespace 级别的,不能按方法粒度控制。
  3. 事务影响

    • 在事务中,缓存行为受事务隔离级别影响。提交事务后缓存才可能被其他 SqlSession 访问。
  4. MyBatis-Plus 兼容性

    • MP 完全兼容 MyBatis 缓存机制。
    • 使用 BaseMapper 方法(如 selectById)时,缓存同样生效。

五、使用技巧

1. 自定义缓存配置

<cache
    type="org.mybatis.caches.redis.RedisCache"
    eviction="LRU"
    flushInterval="60000"
    size="512"
    readOnly="false"/>
  • type:缓存实现类(如 Redis、Ehcache)
  • eviction:回收策略(LRU、FIFO、SOFT、WEAK)
  • flushInterval:刷新间隔(毫秒)
  • size:最多缓存对象数
  • readOnly:是否只读(true 为只读,性能更高)

2. 禁用某个查询的缓存

<select id="selectFreshData" resultType="User" useCache="false">
    SELECT * FROM user WHERE status = 1
</select>

useCache="false" 表示该查询不参与二级缓存。

3. 刷新缓存

<update id="updateUser" parameterType="User" flushCache="true">
    UPDATE user SET name = #{name} WHERE id = #{id}
</update>

flushCache="true" 强制清空缓存(默认 INSERT/UPDATE/DELETE 已为 true)。


六、最佳实践与性能优化

✅ 最佳实践

实践 说明
合理使用一级缓存 在单个业务方法中,避免重复查询相同数据
谨慎使用二级缓存 适用于读多写少、数据变化不频繁的场景(如字典表)
避免缓存雪崩 设置随机过期时间,避免大量缓存同时失效
监控缓存命中率 通过日志或监控工具观察缓存效率
敏感数据不缓存 用户权限、订单状态等频繁变更数据慎用缓存

⚡ 性能优化建议

  1. 选择高效缓存实现

    • 生产环境推荐使用 RedisEhcache 替代默认 PerpetualCache
  2. 控制缓存大小

    • 避免缓存过多数据导致内存溢出。
  3. 使用只读缓存

    • 如果对象不会被修改,设置 readOnly="true",避免序列化开销。
  4. 避免大结果集缓存

    • 分页查询结果可缓存,但全表查询不建议缓存。
  5. 结合本地缓存 + 分布式缓存

    • 一级缓存(本地) + 二级缓存(Redis),形成多级缓存架构。

七、总结对比

特性 一级缓存 二级缓存
作用范围 SqlSession namespace(Mapper)
默认开启 否(需配置)
跨会话共享
存储位置 JVM 内存 可配置(内存、Redis等)
清空时机 SqlSession 关闭/清空 DML 操作、手动清空
适用场景 单次请求内重复查询 跨请求、跨会话的热点数据

八、常见问题 FAQ

Q1:MyBatis-Plus 是否支持缓存?
A:完全支持。MP 基于 MyBatis,缓存机制一致。

Q2:为什么 update 后缓存没有更新?
A:MyBatis 是清空缓存而非“更新”,下次查询会重新加载。

Q3:如何禁用一级缓存?
A:不推荐。可通过 sqlSession.clearCache() 手动控制,或使用 REUSE 执行器(不常用)。

Q4:缓存穿透怎么办?
A:可缓存 null 值(短时间),或使用布隆过滤器预判。