mybatis缓存机制

MyBatis 缓存机制深度剖析与实践指南

MyBatis 的缓存机制是提升数据库访问性能的核心手段,通过减少重复查询来降低数据库压力。深入解析一级缓存、二级缓存的工作原理、适用场景、配置技巧及最佳实践,帮助你在实际项目中合理运用缓存提升系统性能。

缓存机制整体架构

MyBatis 缓存分为一级缓存二级缓存,两者在作用范围、生命周期和使用方式上有显著区别,但协同工作形成完整的缓存体系:

维度 一级缓存(Local Cache) 二级缓存(Global Cache)
作用范围 SqlSession 级别(单次会话) Mapper 接口(namespace)级别
默认状态 自动开启,无法关闭 需手动开启(全局 + 映射文件配置)
数据存储 内存 HashMap(无持久化) 可配置(内存 / 第三方缓存如 Redis)
共享性 仅当前 SqlSession 可见 同 SqlSessionFactory 下所有 SqlSession 共享
失效触发 SqlSession 关闭 / 提交 / 增删改操作 同 namespace 下增删改操作 / 缓存过期

MyBatis 缓存查询流程如下:

  1. 发起查询时,先检查二级缓存(若开启);
  2. 二级缓存未命中,检查一级缓存
  3. 一级缓存未命中,执行数据库查询;
  4. 查询结果依次写入一级缓存和二级缓存(若开启)。

一级缓存:SqlSession 级别的本地缓存

一级缓存是 MyBatis 内置的基础缓存,默认开启且无需额外配置,其核心作用是在单次数据库会话中复用查询结果。

工作原理

  • 存储结构:每个 SqlSession 内部维护一个 HashMap 作为缓存容器,键为 CacheKey,值为查询结果。
  • CacheKey 生成逻辑:由以下因素共同决定(任意一项不同则 CacheKey 不同):
    • MappedStatement 的 ID(即 namespace + methodName);
    • 分页参数(offset/limit);
    • SQL 语句本身;
    • 传入的参数值;
    • 数据库环境 ID(environment.id)。
  • 生命周期:与 SqlSession 一致,随 SqlSession 关闭而销毁。

一级缓存命中与失效示例

示例 1:一级缓存命中(同 SqlSession 内重复查询)
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);

// 第一次查询:数据库查询,结果存入一级缓存
User user1 = mapper.selectById(1); 
// 第二次查询:参数和 SQL 相同,直接从一级缓存获取
User user2 = mapper.selectById(1); 

System.out.println(user1 == user2); // true(同一对象)
session.close();
示例 2:一级缓存失效场景
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);

User user1 = mapper.selectById(1); 

// 场景1:执行增删改操作(自动清空一级缓存)
mapper.updateName(1, "newName"); 
// 场景2:手动清空一级缓存
// session.clearCache(); 
// 场景3:查询参数不同
// User user2 = mapper.selectById(2); 

User user2 = mapper.selectById(1); // 重新查询数据库
System.out.println(user1 == user2); // false
session.close();

一级缓存配置与控制

  • 禁用一级缓存:通过全局配置 localCacheScope 控制,默认值为 SESSION(开启一级缓存),设置为 STATEMENT 可禁用(每次查询后立即清空缓存):

    <settings>
      <!-- 禁用一级缓存:每次查询都走数据库 -->
      <setting name="localCacheScope" value="STATEMENT"/>
    </settings>
  • 适用场景

    • 单次会话内需要重复查询相同数据(如表单校验、数据展示);
    • 避免短时间内重复访问数据库,减轻瞬时压力。

二级缓存:跨 SqlSession 的全局缓存

二级缓存是基于 namespace(Mapper 接口)的全局缓存,可跨 SqlSession 共享数据,适合查询频繁、更新较少的数据(如字典表、配置表)。

开启与配置二级缓存

二级缓存需要两步配置才能生效:

步骤 1:全局开启二级缓存(默认已开启,可显式配置)
<settings>
  <!-- 开启二级缓存(默认true,显式配置更清晰) -->
  <setting name="cacheEnabled" value="true"/>
</settings>
步骤 2:在 Mapper 映射文件中声明缓存
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
  <!-- 配置二级缓存 -->
  <cache 
    eviction="LRU"        <!-- 缓存回收策略LRU默认)/FIFO/SOFT/WEAK -->
    flushInterval="60000" <!-- 缓存刷新间隔(毫秒),默认不清空 -->
    size="1024"           <!-- 最大缓存条目,默认1024 -->
    readOnly="false"      <!-- 是否只读:false(默认,支持序列化克隆) -->
    blocking="false"/>    <!-- 缓存未命中时是否阻塞等待 -->
</mapper>

二级缓存核心配置解析

配置项 作用说明
eviction 缓存回收策略: - LRU(最近最少使用):移除最长时间未访问的条目(推荐); - FIFO(先进先出):按插入顺序移除最早条目; - SOFT(软引用):基于 JVM 软引用规则回收(内存不足时); - WEAK(弱引用):基于 JVM 弱引用规则回收(GC 时立即回收)。
flushInterval 缓存自动清空间隔(毫秒),默认 null(永不自动清空),适合定时更新的数据。
size 最大缓存条目数,需根据内存大小调整(避免 OOM)。
readOnly - true:返回缓存对象的引用(性能高,线程不安全); - false:返回对象的序列化克隆(性能略低,线程安全,默认)。
blocking 缓存未命中时是否阻塞(true 则等待其他线程加载完成,避免缓存击穿)。

二级缓存工作流程与示例

工作流程:
  1. SqlSession1 查询数据 → 写入一级缓存;
  2. SqlSession1 提交(commit())→ 一级缓存数据写入二级缓存;
  3. SqlSession2 查询相同数据 → 直接从二级缓存获取。
示例:二级缓存命中(跨 SqlSession 共享)
// 实体类必须实现 Serializable(readOnly=false 时需要序列化)
public class User implements Serializable {
  private Integer id;
  private String name;
  // getters/setters
}

// 测试代码
@Test
public void testSecondLevelCache() {
  // 第一个 SqlSession
  SqlSession session1 = sqlSessionFactory.openSession();
  UserMapper mapper1 = session1.getMapper(UserMapper.class);
  User user1 = mapper1.selectById(1); // 查数据库
  session1.commit(); // 提交后数据写入二级缓存
  session1.close();  // 一级缓存销毁,但二级缓存已存在

  // 第二个 SqlSession
  SqlSession session2 = sqlSessionFactory.openSession();
  UserMapper mapper2 = session2.getMapper(UserMapper.class);
  User user2 = mapper2.selectById(1); // 从二级缓存获取
  session2.close();

  System.out.println(user1 == user2); // false(readOnly=false 时为克隆对象)
}

二级缓存失效场景

  • 未提交事务SqlSession 未调用 commit(),一级缓存数据不会写入二级缓存;

  • 同 namespace 下的增删改操作:增删改标签默认 flushCache="true",执行后会清空当前 namespace 的二级缓存;

  • 查询标签配置 useCache="false":强制禁用当前查询的二级缓存(一级缓存仍生效):

    <select id="selectById" useCache="false"> <!-- 不使用二级缓存 -->
      SELECT * FROM user WHERE id = #{id}
    </select>
  • 跨 namespace 操作:二级缓存是 namespace 隔离的,其他 Mapper 的操作不会影响当前缓存。

自定义二级缓存:集成第三方缓存

MyBatis 二级缓存支持自定义实现,通过实现 org.apache.ibatis.cache.Cache 接口,可集成 Redis、EHCache 等专业缓存框架(解决内存缓存的容量限制和持久化问题)。

示例:集成 Redis 作为二级缓存

步骤 1:实现 Cache 接口
public class RedisCache implements Cache {
  private final String id; // namespace 作为缓存唯一标识
  private final RedisTemplate<String, Object> redisTemplate;
  private final long expireTime = 30 * 60; // 缓存过期时间(30分钟)

  // 构造方法:MyBatis 会自动传入 namespace 作为 id
  public RedisCache(String id) {
    this.id = id;
    // 获取 Spring 容器中的 RedisTemplate(需提前配置)
    this.redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
  }

  @Override
  public String getId() {
    return id;
  }

  // 存入缓存(key 为 CacheKey,value 为查询结果)
  @Override
  public void putObject(Object key, Object value) {
    redisTemplate.opsForValue()
      .set(key.toString(), value, expireTime, TimeUnit.SECONDS);
  }

  // 获取缓存
  @Override
  public Object getObject(Object key) {
    return redisTemplate.opsForValue().get(key.toString());
  }

  // 移除缓存(默认不实现,由 Redis 过期策略处理)
  @Override
  public Object removeObject(Object key) {
    return null;
  }

  // 清空缓存(增删改时触发)
  @Override
  public void clear() {
    Set<String> keys = redisTemplate.keys(id + ":*"); // 按 namespace 前缀清除
    if (keys != null && !keys.isEmpty()) {
      redisTemplate.delete(keys);
    }
  }

  // 获取缓存大小(可选实现)
  @Override
  public int getSize() {
    return 0;
  }
}
步骤 2:在 Mapper 中引用自定义缓存
<mapper namespace="com.example.mapper.UserMapper">
  <!-- 使用 Redis 作为二级缓存 -->
  <cache type="com.example.cache.RedisCache" />
</mapper>

缓存使用最佳实践

1. 一级缓存使用建议

  • 默认保留:一级缓存无需额外配置,能有效优化单次会话内的重复查询;
  • 避免长会话:长时间持有 SqlSession 会导致一级缓存膨胀,建议用完即关;
  • 禁用场景:实时性要求极高的查询(如股票行情),可通过 localCacheScope=STATEMENT 禁用。

2. 二级缓存使用建议

  • 适合场景:查询频繁、更新较少的数据(如字典表、地区表);
  • 不适合场景:实时性要求高的数据(如订单状态)、频繁更新的表;
  • 序列化要求readOnly=false 时,实体类必须实现 Serializable 接口;
  • 缓存粒度:避免在大表对应的 Mapper 中开启二级缓存(缓存条目过多导致性能下降)。

3. 缓存问题规避

  • 缓存穿透:对不存在的 key 频繁查询(如恶意攻击),可通过布隆过滤器过滤无效 key;
  • 缓存击穿:热点 key 过期瞬间的大量并发查询,可配置 blocking=true 或设置永不过期;
  • 缓存雪崩:大量缓存同时过期,可设置随机过期时间(如 expireTime + 随机数)避免集中失效。

4. 缓存配置速查表

配置项 作用范围 默认值 说明
cacheEnabled 全局(二级缓存) true 开启 / 关闭所有二级缓存
localCacheScope 全局(一级缓存) SESSION SESSION(开启)/STATEMENT(禁用)
<cache> Mapper 级别 声明二级缓存并配置参数
useCache 查询标签 true 当前查询是否使用二级缓存
flushCache 增删改标签 true 执行后是否清空一级 + 二级缓存
flushCache 查询标签 false 执行后是否清空缓存(true 则禁用缓存)

总结

MyBatis 缓存机制通过一级缓存(SqlSession 级别)和二级缓存(namespace 级别)的协同,有效减少了数据库访问次数。实际开发中,应根据数据特性选择合适的缓存策略:

  • 短期会话内的重复查询依赖一级缓存;
  • 跨会话共享的低频更新数据使用二级缓存,并优先集成 Redis 等第三方缓存框架;
  • 实时性要求高或频繁更新的数据应禁用缓存,避免数据不一致