0%

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 内重复查询)
1
2
3
4
5
6
7
8
9
10
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:一级缓存失效场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 可禁用(每次查询后立即清空缓存):

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

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

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

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

开启与配置二级缓存

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

步骤 1:全局开启二级缓存(默认已开启,可显式配置)
1
2
3
4
<settings>
<!-- 开启二级缓存(默认true,显式配置更清晰) -->
<setting name="cacheEnabled" value="true"/>
</settings>
步骤 2:在 Mapper 映射文件中声明缓存
1
2
3
4
5
6
7
8
9
10
<!-- 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 共享)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 实体类必须实现 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":强制禁用当前查询的二级缓存(一级缓存仍生效):

    1
    2
    3
    <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 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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 中引用自定义缓存
1
2
3
4
<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 等第三方缓存框架;
  • 实时性要求高或频繁更新的数据应禁用缓存,避免数据不一致

欢迎关注我的其它发布渠道