MyBatis 缓存机制
缓存体系架构
MyBatis 提供两级缓存:
┌─────────────────────────────────────────┐
│ 应用程序 │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 二级缓存(SqlSessionFactory 级别) │
│ 跨 Session 共享,需要配置开启 │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 一级缓存(SqlSession 级别) │
│ Session 内共享,默认开启 │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 数据库 │
└─────────────────────────────────────────┘一级缓存
基本特性
| 特性 | 说明 |
|---|---|
| 作用范围 | SqlSession 级别 |
| 默认状态 | 开启,无法关闭 |
| 生命周期 | SqlSession 生命周期 |
| 数据结构 | HashMap(本地内存) |
工作原理
// 同一个 SqlSession 内
SqlSession session = sqlSessionFactory.openSession();
User user1 = session.selectOne("getUserById", 1); // 查询数据库,缓存结果
User user2 = session.selectOne("getUserById", 1); // 命中缓存
System.out.println(user1 == user2); // true,同一个对象
session.close();执行流程:
查询请求 → 检查一级缓存 → 命中? → 返回缓存对象
↓ 未命中
查询数据库 → 写入缓存 → 返回结果缓存 Key 组成
一级缓存 Key 由以下因素决定:
CacheKey = hashCode + mappedStatementId + offset + limit + sql + parameter + environmentId相同查询条件才会命中缓存:
// 同一个 Session,相同 SQL 和参数 → 命中缓存
User user1 = session.selectOne("getUserById", 1);
User user2 = session.selectOne("getUserById", 1);
// 不同参数 → 不命中
User user3 = session.selectOne("getUserById", 2);缓存失效场景
SqlSession session = sqlSessionFactory.openSession();
// 1. 执行 INSERT/UPDATE/DELETE
session.insert("insertUser", user);
User user = session.selectOne("getUserById", 1); // 缓存失效,查数据库
// 2. 调用 session.clearCache()
session.clearCache();
// 3. 调用 session.commit()
session.commit(); // 清空一级缓存
// 4. 调用 session.rollback()
session.rollback();
// 5. SqlSession 关闭
session.close();一级缓存问题
问题:不同 Session 无法共享缓存
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
User user1 = session1.selectOne("getUserById", 1); // 查数据库
User user2 = session2.selectOne("getUserById", 1); // 又查数据库!
// session1 和 session2 各自独立,缓存不共享二级缓存
基本特性
| 特性 | 说明 |
|---|---|
| 作用范围 | SqlSessionFactory 级别(命名空间) |
| 默认状态 | 关闭,需要配置开启 |
| 生命周期 | 应用生命周期 |
| 跨 Session | 可共享 |
开启配置
1. 全局配置(mybatis-config.xml):
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>2. Mapper 配置(UserMapper.xml):
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache/>
<!-- 或自定义缓存配置 -->
<cache
eviction="LRU" <!-- 淘汰策略 -->
flushInterval="60000" <!-- 刷新间隔(毫秒) -->
size="1024" <!-- 缓存大小 -->
readOnly="true"/> <!-- 只读 -->
</mapper>3. 实体类实现序列化:
public class User implements Serializable {
private Integer id;
private String name;
// ...
}工作原理
// 不同 SqlSession,相同查询 → 命中二级缓存
SqlSession session1 = sqlSessionFactory.openSession();
User user1 = session1.selectOne("getUserById", 1); // 查数据库
session1.commit(); // 提交后写入二级缓存
SqlSession session2 = sqlSessionFactory.openSession();
User user2 = session2.selectOne("getUserById", 1); // 命中二级缓存
System.out.println(user1 == user2); // false,但数据相同(可能是序列化副本)执行流程:
查询请求 → 检查二级缓存 → 命中? → 返回缓存对象
↓ 未命中
检查一级缓存 → 命中? → 返回缓存对象
↓ 未命中
查询数据库 → 写入一级缓存 → commit后写入二级缓存缓存淘汰策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| LRU(默认) | 最近最少使用 | 通用场景 |
| FIFO | 先进先出 | 顺序访问 |
| SOFT | 软引用,内存不足时清除 | 内存敏感 |
| WEAK | 弱引用,GC 时清除 | 内存紧张 |
<!-- 配置 LRU 策略 -->
<cache eviction="LRU" size="1024"/>二级缓存失效
// 在同一个命名空间内执行增删改
session.insert("insertUser", user);
session.commit(); // 该命名空间的二级缓存清空
// 不同命名空间的操作不会影响
session.insert("com.example.mapper.OrderMapper.insertOrder", order);
session.commit(); // UserMapper 的缓存不受影响注意事项
1. 关联查询缓存问题:
<!-- UserMapper.xml -->
<cache/>
<!-- 关联查询 order 表 -->
<select id="getUserWithOrders" resultMap="userOrderMap">
SELECT u.*, o.* FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
<!-- 问题:OrderMapper 的更新不会清空 UserMapper 的缓存! -->解决方案:使用 cache-ref
<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
<!-- 引用 UserMapper 的缓存,共享缓存空间 -->
<cache-ref namespace="com.example.mapper.UserMapper"/>
</mapper>2. 分布式环境问题:
二级缓存是本地缓存,分布式环境下各节点缓存不一致。
解决方案:
- 使用分布式缓存(Redis)
- 或关闭二级缓存,使用分布式缓存层
一级缓存 vs 二级缓存
| 对比项 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用范围 | SqlSession | SqlSessionFactory(命名空间) |
| 默认状态 | 开启 | 关闭 |
| 跨 Session | ❌ 不共享 | ✅ 共享 |
| 生命周期 | Session 生命周期 | 应用生命周期 |
| 失效时机 | 增删改、commit、rollback | 命名空间内增删改 |
| 存储位置 | JVM 内存 | JVM 内存(可配置外部) |
自定义缓存(Redis)
实现 Cache 接口
public class RedisCache implements Cache {
private final String id;
private RedisTemplate<String, Object> redisTemplate;
public RedisCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
getRedisTemplate().opsForValue().set(key.toString(), value, 30, TimeUnit.MINUTES);
}
@Override
public Object getObject(Object key) {
return getRedisTemplate().opsForValue().get(key.toString());
}
@Override
public Object removeObject(Object key) {
getRedisTemplate().delete(key.toString());
return null;
}
@Override
public void clear() {
getRedisTemplate().delete(getRedisTemplate().keys("*" + id + "*"));
}
@Override
public int getSize() {
return getRedisTemplate().keys("*" + id + "*").size();
}
}配置使用
<!-- UserMapper.xml -->
<cache type="com.example.cache.RedisCache"/>缓存使用建议
适合使用缓存的场景
- 读多写少:数据变更频率低
- 实时性要求不高:可以接受短暂的数据不一致
- 查询结果小:避免占用过多内存
不适合使用缓存的场景
- 写多读少:缓存频繁失效
- 实时性要求高:金融交易等
- 查询结果大:大结果集占用内存
- 分布式环境:缓存不一致问题
最佳实践
// 1. 查询频繁但变更少的数据 → 开启二级缓存
// 2. 单次请求内的重复查询 → 自动使用一级缓存
// 3. 分布式环境 → 使用 Redis 等分布式缓存
// 4. 复杂关联查询 → 考虑 cache-ref 或手动缓存面试高频问题
Q1: 一级缓存和二级缓存的区别?
| 对比项 | 一级缓存 | 二级缓存 |
|---|---|---|
| 范围 | SqlSession | SqlSessionFactory |
| 开启 | 默认开启 | 需配置开启 |
| 共享 | Session 内 | 跨 Session |
| 生命周期 | Session | 应用 |
Q2: 为什么一级缓存无法关闭?
设计理念:同一 Session 内的重复查询应该避免多次访问数据库。
可以通过 session.clearCache() 或 statement.flushCache=true 清空。
Q3: 二级缓存的问题和解决方案?
问题:
- 细粒度控制差(命名空间级别)
- 分布式环境不一致
- 关联表更新问题
解决方案:
- 使用
cache-ref共享缓存 - 使用 Redis 分布式缓存
- 复杂场景手动缓存控制
Q4: 如何选择缓存策略?
单机环境 + 读多写少 → 二级缓存
分布式环境 → Redis 分布式缓存
实时性要求高 → 不使用缓存 / 短过期时间总结
MyBatis 缓存要点:
1. 一级缓存:Session 级别,默认开启,增删改后失效
2. 二级缓存:SessionFactory 级别,需配置,跨 Session 共享
3. 淘汰策略:LRU/FIFO/SOFT/WEAK
4. 分布式问题:二级缓存是本地缓存,需用 Redis 替代