首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >详细介绍一下MyBatis的缓存机制

详细介绍一下MyBatis的缓存机制

原创
作者头像
小焱写作
发布2025-11-13 08:49:14
发布2025-11-13 08:49:14
1100
举报
文章被收录于专栏:javajava

MyBatis 缓存机制详解(一级缓存+二级缓存+自定义缓存)

MyBatis 的缓存机制是其核心性能优化手段之一,目的是减少数据库查询次数,降低IO开销,提升查询效率。其设计遵循“分层缓存”理念,分为 一级缓存(SqlSession 级别)二级缓存(Mapper 级别),同时支持集成第三方缓存(如 Redis)实现分布式场景下的缓存共享。

一、缓存核心设计理念

  1. 缓存粒度:从“会话级”到“跨会话级”,逐步扩大缓存作用范围;
  2. 失效策略:基于“数据一致性”优先,增删改操作会触发对应缓存失效;
  3. 存储介质:默认基于内存(HashMap),支持自定义扩展(如磁盘、分布式缓存);
  4. 命中规则:缓存 Key 由“SQL语句+参数+环境信息+Mapper 信息”组成,确保缓存精准命中。

二、一级缓存(SqlSession 级别缓存)

1. 基本定义

一级缓存是 SqlSession 实例级别的缓存,即同一个 SqlSession 内执行相同的查询操作,只会第一次访问数据库,后续直接从内存中获取结果。

2. 核心特性
  • 默认开启:无需任何配置,MyBatis 自动启用,无法手动关闭;
  • 作用范围:仅当前 SqlSession 有效,不同 SqlSession 之间的缓存相互隔离;
  • 存储介质:内存中的 HashMap,键(Key)是缓存唯一标识,值(Value)是查询结果对象;
  • 线程不安全:SqlSession 是线程私有对象,不存在多线程并发访问缓存的问题。
3. 缓存 Key 的构成(确保查询唯一性)

一级缓存的 Key 由以下 4 部分组成,缺一不可:

代码语言:javascript
复制
Key = HashMap<Object, Object> {
    "MappedStatementId": Mapper接口全类名+方法名(如com.example.mapper.UserMapper.selectById),
    "SQL语句": 最终执行的SQL(含动态SQL拼接结果),
    "参数": 查询参数(如id=1001),
    "环境信息": 数据库连接环境(如数据源ID、事务状态)
}
4. 缓存命中与失效场景
(1)命中场景
  • 同一个 SqlSession 内,执行 相同的查询方法+相同参数+相同环境
  • 两次查询之间没有执行该表的增删改操作(insert/update/delete);
  • 两次查询之间没有手动清除缓存(​​sqlSession.clearCache()​​)。
(2)失效场景(重点面试考点)
  1. 执行 ​​sqlSession.close()​​:关闭 SqlSession 时,一级缓存会被销毁;
  2. 执行增删改操作(​​insert()​​/​​update()​​/​​delete()​​):MyBatis 会自动清除当前 SqlSession 内的所有一级缓存(避免数据不一致);
  3. 手动清除缓存:调用 ​​sqlSession.clearCache()​​ 方法,主动清空当前 SqlSession 的缓存;
  4. 跨 SqlSession 查询:不同 SqlSession 之间的缓存相互独立,无法共享;
  5. 查询参数/方法不同:即使是同一 Mapper 方法,参数不同或方法名不同,也会生成不同的缓存 Key;
  6. 开启全局二级缓存:一级缓存依然生效,但查询结果会同步写入二级缓存(不影响一级缓存命中)。
5. 一级缓存执行流程(示例)
代码语言:javascript
复制
// 1. 获取 SqlSession(默认不自动提交事务)
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

// 2. 第一次查询:访问数据库,结果存入一级缓存
User user1 = userMapper.selectById(1001); 
System.out.println(user1); // 数据库查询

// 3. 第二次查询:参数相同,直接从一级缓存获取
User user2 = userMapper.selectById(1001); 
System.out.println(user2); // 缓存命中,无数据库查询

// 4. 执行更新操作:触发一级缓存清空
userMapper.updateName(1001, "新名字");
sqlSession.commit();

// 5. 第三次查询:缓存已失效,重新访问数据库
User user3 = userMapper.selectById(1001); 
System.out.println(user3); // 数据库查询

// 6. 关闭 SqlSession:一级缓存销毁
sqlSession.close();

三、二级缓存(Mapper 级别缓存)

1. 基本定义

二级缓存是 Mapper 接口级别的缓存,即同一个 Mapper 接口下的所有 SqlSession 共享缓存(跨 SqlSession 共享)。例如,UserMapper 的二级缓存可被多个 SqlSession 访问,适合查询频繁、修改少的场景。

2. 核心特性
  • 默认关闭:需手动配置开启;
  • 作用范围:同一个 Mapper 接口(namespace 相同),跨 SqlSession 共享;
  • 存储介质:默认是内存 HashMap,支持配置为磁盘存储或第三方缓存(如 Redis);
  • 线程安全:MyBatis 内部通过锁机制保证多线程并发访问缓存的安全性;
  • 序列化要求:缓存的实体类必须实现 ​​Serializable​​ 接口(默认缓存会序列化存储,避免对象引用问题)。
3. 二级缓存的开启与配置
(1)全局配置开启(可选,默认已开启)

在 ​​mybatis-config.xml​​ 中配置 ​​cacheEnabled​​(默认值为 ​​true​​,可省略):

代码语言:javascript
复制
<configuration>
  <settings>
    <!-- 开启二级缓存总开关(关闭后所有 Mapper 的二级缓存都失效) -->
    <setting name="cacheEnabled" value="true"/>
  </settings>
</configuration>
(2)Mapper 级别开启(必须配置)

在对应的 ​​Mapper.xml​​ 中添加 ​​<cache>​​ 标签,开启当前 Mapper 的二级缓存:

代码语言:javascript
复制
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
  <!-- 开启二级缓存,配置缓存参数 -->
  <cache 
    eviction="LRU"          <!-- 缓存回收策略(默认 LRU) -->
    flushInterval="60000"    <!-- 缓存过期时间(毫秒,60秒) -->
    size="1024"              <!-- 缓存最大条目数(默认 1024) -->
    readOnly="true"          <!-- 是否只读(默认 false) -->
    blocking="false"         <!-- 是否阻塞(缓存未命中时等待其他线程查询结果,默认 false) -->
  />

  <!-- 单个查询方法可单独控制是否使用二级缓存(默认使用) -->
  <select id="selectById" resultType="User" useCache="true">
    select * from user where id = #{id}
  </select>

  <!-- 增删改方法可控制是否触发缓存清空(默认触发) -->
  <update id="updateName" flushCache="true">
    update user set name = #{name} where id = #{id}
  </update>
</mapper>
(3)关键配置参数说明

参数名

取值范围

作用说明

​​eviction​​

LRU(默认)/FIFO/SOFT/WEAK

缓存回收策略:- LRU:最近最少使用(移除最长时间未被访问的缓存);- FIFO:先进先出(按缓存添加顺序移除);- SOFT:软引用(JVM内存不足时移除);- WEAK:弱引用(垃圾回收时直接移除)。

​​flushInterval​​

数值(毫秒)

缓存自动过期时间,默认无过期(一直有效)。

​​size​​

正整数

缓存最大存储条目数,超出后按回收策略移除旧缓存(避免内存溢出)。

​​readOnly​​

true/false(默认)

- true:缓存返回对象本身(性能高,但线程不安全,需确保对象不被修改);- false:缓存返回对象的拷贝(通过序列化,线程安全,性能略低)。

​​blocking​​

true/false(默认)

- true:缓存未命中时,其他线程需等待当前线程查询数据库并写入缓存(避免缓存穿透);- false:缓存未命中时,所有线程均访问数据库(可能导致并发查询)。

(4)实体类序列化要求

二级缓存默认会对查询结果进行序列化存储(即使 ​​readOnly="true"​​),因此实体类必须实现 ​​Serializable​​ 接口,否则会抛出 ​​NotSerializableException​​ 异常:

代码语言:javascript
复制
// 实体类实现 Serializable 接口
public class User implements Serializable {
  private Long id;
  private String name;
  // getter/setter...
}
4. 二级缓存命中与失效场景
(1)命中场景
  • 同一个 Mapper 接口(namespace 相同);
  • 不同 SqlSession 执行相同的查询方法+相同参数;
  • 两次查询之间没有执行该 Mapper 下的增删改操作;
  • 缓存未过期且未达到最大条目数;
  • 开启了二级缓存(全局+Mapper 级别均开启)。
(2)失效场景(重点面试考点)
  1. 执行增删改操作:当前 Mapper 下的任何 ​​insert()​​/​​update()​​/​​delete()​​ 操作(无论是否修改目标数据),都会清空该 Mapper 的所有二级缓存;
  2. 手动清空缓存:调用 ​​sqlSession.clearCache()​​(仅清空当前 SqlSession 的一级缓存)或 ​​sqlSessionFactory.getConfiguration().getCache("namespace").clear()​​(清空指定 Mapper 的二级缓存);
  3. 未开启二级缓存:全局开关 ​​cacheEnabled="false"​​ 或 Mapper 未配置 ​​<cache>​​ 标签;
  4. 查询方法禁用二级缓存:​​select​​ 标签设置 ​​useCache="false"​​(如实时性要求高的查询);
  5. 增删改方法未触发缓存清空:​​insert​​/​​update​​/​​delete​​ 标签设置 ​​flushCache="false"​​(不推荐,会导致数据不一致);
  6. 实体类未实现 ​​Serializable​​ 接口:缓存写入失败,无法命中;
  7. 跨 Mapper 查询:不同 Mapper 接口的缓存相互独立,无法共享(如 UserMapper 和 OrderMapper 的缓存不互通)。
5. 二级缓存执行流程(示例)
代码语言:javascript
复制
// 1. 第一个 SqlSession:查询并写入二级缓存
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1001); // 数据库查询,结果存入一级缓存+二级缓存
sqlSession1.close(); // 关闭 SqlSession 时,一级缓存销毁,二级缓存保留

// 2. 第二个 SqlSession:共享二级缓存
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1001); // 二级缓存命中,无数据库查询
sqlSession2.close();

// 3. 第三个 SqlSession:执行更新操作,触发二级缓存清空
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
mapper3.updateName(1001, "新名字");
sqlSession3.commit(); // 提交事务,清空 UserMapper 的二级缓存
sqlSession3.close();

// 4. 第四个 SqlSession:缓存失效,重新查询
SqlSession sqlSession4 = sqlSessionFactory.openSession();
UserMapper mapper4 = sqlSession4.getMapper(UserMapper.class);
User user4 = mapper4.selectById(1001); // 二级缓存已失效,数据库查询
sqlSession4.close();

四、一级缓存与二级缓存的区别(面试高频)

对比维度

一级缓存(SqlSession 级别)

二级缓存(Mapper 级别)

开启方式

默认开启,无需配置

默认关闭,需全局+Mapper 配置

作用范围

单个 SqlSession(线程私有)

同一个 Mapper 接口(跨 SqlSession)

存储介质

内存 HashMap(非序列化)

内存 HashMap/磁盘/第三方缓存(需序列化)

实体类要求

无序列化要求

必须实现 Serializable 接口

失效触发

关闭 SqlSession、增删改、手动清空

增删改操作、缓存过期、手动清空

数据一致性

高(会话内隔离,修改后缓存清空)

中(跨会话共享,需依赖失效机制)

适用场景

单次会话内重复查询(如表单校验)

多会话共享查询(如字典数据、静态数据)

线程安全

安全(线程私有)

安全(内部锁机制)

核心结论:一级缓存是“会话内优化”,二级缓存是“跨会话优化”,二者协同工作(查询时先查一级缓存→再查二级缓存→最后查数据库)。

五、自定义缓存(集成第三方缓存)

MyBatis 的默认二级缓存是内存级缓存,存在以下局限性:

  • 不支持分布式场景(多服务实例无法共享缓存);
  • 内存有限,无法存储大量数据;
  • 服务重启后缓存丢失。

因此,实际开发中常集成 RedisEhcache 等第三方缓存替代默认二级缓存,MyBatis 提供了 ​​Cache​​ 接口,支持自定义缓存实现。

1. 集成 Redis 实现二级缓存(实战示例)
(1)核心依赖(Maven)
代码语言:javascript
复制
<!-- MyBatis 缓存接口 -->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.13</version>
</dependency>
<!-- Redis 客户端 -->
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>4.4.6</version>
</dependency>
<!-- 序列化工具(可选,用于对象序列化) -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson2</artifactId>
  <version>2.0.41</version>
</dependency>
(2)实现 MyBatis 的 Cache 接口
代码语言:javascript
复制
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 自定义 Redis 缓存实现
 */
public class RedisCache implements Cache {
  // 缓存 ID(对应 Mapper 的 namespace)
  private final String id;
  // Redis 连接池
  private final JedisPool jedisPool;
  // 读写锁(保证线程安全)
  private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

  // 构造方法:MyBatis 会自动传入 Mapper 的 namespace 作为 id
  public RedisCache(String id) {
    if (id == null) {
      throw new IllegalArgumentException("Cache ID cannot be null");
    }
    this.id = id;
    // 初始化 Redis 连接池(实际开发中应通过配置文件读取参数)
    this.jedisPool = new JedisPool("localhost", 6379);
  }

  @Override
  public String getId() {
    return id; // 缓存唯一标识(必须返回 Mapper 的 namespace)
  }

  // 存入缓存:key 是 MyBatis 生成的缓存 Key,value 是查询结果
  @Override
  public void putObject(Object key, Object value) {
    try (Jedis jedis = jedisPool.getResource()) {
      // 序列化 key 和 value(使用 fastjson2)
      String redisKey = key.toString();
      String redisValue = com.alibaba.fastjson2.JSON.toJSONString(value);
      // 存入 Redis,设置过期时间(30分钟)
      jedis.setex(redisKey, 1800, redisValue);
    }
  }

  // 获取缓存:根据 key 查询 Redis
  @Override
  public Object getObject(Object key) {
    try (Jedis jedis = jedisPool.getResource()) {
      String redisKey = key.toString();
      String redisValue = jedis.get(redisKey);
      if (redisValue == null) {
        return null;
      }
      // 反序列化:根据实际返回类型转换(此处简化处理,实际需结合 TypeHandler)
      return com.alibaba.fastjson2.JSON.parseObject(redisValue, Object.class);
    }
  }

  // 移除缓存(MyBatis 内部很少调用,可空实现)
  @Override
  public Object removeObject(Object key) {
    try (Jedis jedis = jedisPool.getResource()) {
      return jedis.del(key.toString());
    }
  }

  // 清空缓存(增删改操作时触发)
  @Override
  public void clear() {
    try (Jedis jedis = jedisPool.getResource()) {
      // 模糊删除当前 Mapper 的所有缓存(key 前缀为 id + ":")
      jedis.keys(id + ":*").forEach(jedis::del);
    }
  }

  // 获取缓存大小(可选实现)
  @Override
  public int getSize() {
    try (Jedis jedis = jedisPool.getResource()) {
      return Math.toIntExact(jedis.keys(id + ":*").size());
    }
  }

  // 获取读写锁(MyBatis 用于并发控制)
  @Override
  public ReadWriteLock getReadWriteLock() {
    return readWriteLock;
  }
}
(3)在 Mapper.xml 中配置自定义缓存
代码语言:javascript
复制
<mapper namespace="com.example.mapper.UserMapper">
  <!-- 配置 Redis 缓存实现(替代默认缓存) -->
  <cache type="com.example.cache.RedisCache">
    <!-- 可自定义缓存参数(通过构造方法或 setter 注入) -->
  </cache>

  <!-- 查询方法使用 Redis 缓存 -->
  <select id="selectById" resultType="User" useCache="true">
    select * from user where id = #{id}
  </select>
</mapper>
2. 自定义缓存的核心注意事项
  • 缓存 Key 唯一性:必须基于 ​​namespace + SQL + 参数​​ 生成 Key,避免不同 Mapper 缓存冲突;
  • 序列化/反序列化:确保实体类可序列化,且反序列化时类型匹配(可结合 MyBatis 的 ​​TypeHandler​​ 优化);
  • 过期时间设置:避免缓存长期有效导致数据不一致,建议设置合理的过期时间(如 30 分钟~1 小时);
  • 分布式锁:分布式场景下,需通过 Redis 分布式锁(如 RedLock)优化并发问题(避免缓存穿透/击穿);
  • 连接池管理:Redis 连接池需合理配置(最大连接数、空闲时间),避免连接泄露。

六、缓存机制的最佳实践(面试+实战)

1. 适用场景
  • 一级缓存:默认启用,无需额外配置,适合单次会话内重复查询(如表单提交前的校验、批量操作中的重复查询);
  • 二级缓存:适合查询频繁、修改少、实时性要求低的数据(如字典表、地区表、静态配置数据);
  • 第三方缓存(Redis):分布式系统、微服务架构,需要跨服务共享缓存的场景。
2. 避坑指南
  • 禁止在高频修改的数据上使用二级缓存(如用户表、订单表),否则会频繁触发缓存失效,反而降低性能;
  • 多表关联查询不建议使用二级缓存(如 ​​select * from user u join order o on u.id = o.user_id​​),因为关联表的增删改操作无法触发当前 Mapper 的缓存清空,会导致数据不一致;
  • 实体类必须实现 ​​Serializable​​ 接口(二级缓存/第三方缓存均需序列化);
  • 分布式场景下,必须使用第三方缓存(如 Redis),默认二级缓存无法跨服务共享;
  • 避免缓存穿透:对查询结果为 ​​null​​ 的数据也存入缓存(设置短期过期),防止恶意查询不存在的 ID 击垮数据库;
  • 避免缓存击穿:热点数据设置永不过期,或通过互斥锁(如 Redis 的 ​​setnx​​)控制并发查询。
3. 性能优化建议
  • 合理设置二级缓存的过期时间(​​flushInterval​​):根据数据更新频率调整,如字典表可设置 1 小时,新闻列表可设置 5 分钟;
  • 限制缓存条目数(​​size​​):避免缓存过大导致内存溢出,建议设置为 1024~4096;
  • 开启缓存只读模式(​​readOnly="true"​​):如果查询结果无需修改,开启只读模式可提升性能(避免对象拷贝);
  • 结合 MyBatis 分页插件(如 PageHelper):分页查询的缓存 Key 会包含分页参数,避免缓存混淆;
  • 监控缓存命中率:通过自定义缓存统计命中次数,优化缓存策略(如命中率低于 30%,建议关闭二级缓存)。

七、面试核心考点总结

  1. 缓存分层:一级缓存(SqlSession 级,默认开启)和二级缓存(Mapper 级,默认关闭)的区别;
  2. 失效机制:增删改操作会触发缓存清空,跨 SqlSession 仅二级缓存可共享;
  3. 二级缓存配置:全局开关 ​​cacheEnabled​​ + Mapper 标签 ​​<cache>​​ + 实体类 ​​Serializable​​;
  4. 自定义缓存:实现 ​​Cache​​ 接口,集成 Redis 等第三方缓存的步骤;
  5. 最佳实践:哪些场景适合用二级缓存,哪些场景禁用,如何避免数据不一致。

MyBatis 缓存机制的核心是“平衡性能与数据一致性”,实际开发中需根据业务场景选择合适的缓存策略,避免盲目开启二级缓存导致的问题。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • MyBatis 缓存机制详解(一级缓存+二级缓存+自定义缓存)
    • 一、缓存核心设计理念
    • 二、一级缓存(SqlSession 级别缓存)
      • 1. 基本定义
      • 2. 核心特性
      • 3. 缓存 Key 的构成(确保查询唯一性)
      • 4. 缓存命中与失效场景
      • 5. 一级缓存执行流程(示例)
    • 三、二级缓存(Mapper 级别缓存)
      • 1. 基本定义
      • 2. 核心特性
      • 3. 二级缓存的开启与配置
      • 4. 二级缓存命中与失效场景
      • 5. 二级缓存执行流程(示例)
    • 四、一级缓存与二级缓存的区别(面试高频)
    • 五、自定义缓存(集成第三方缓存)
      • 1. 集成 Redis 实现二级缓存(实战示例)
      • 2. 自定义缓存的核心注意事项
    • 六、缓存机制的最佳实践(面试+实战)
      • 1. 适用场景
      • 2. 避坑指南
      • 3. 性能优化建议
    • 七、面试核心考点总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档