Appearance
MyBatis 缓存机制
MyBatis 提供两级缓存机制来减少数据库访问次数,提升查询性能。理解缓存的作用域和失效机制是优化 MyBatis 性能的关键。
一、缓存架构图
MyBatis 缓存层级:
请求 1 请求 2
│ │
▼ ▼
SqlSession 1 SqlSession 2
│ │
├── 一级缓存(SqlSession 级别) ├── 一级缓存(SqlSession 级别)
│ 默认开启,不可关闭 │ 默认开启,不可关闭
│ 作用域:同一个 SqlSession │ 作用域:同一个 SqlSession
│ │
└──────┬──────────────┘
│
▼
┌─────────────────────────────────┐
│ 二级缓存(Mapper 级别) │
│ 作用域:同一个 namespace 的所有 │
│ SqlSession 共享 │
│ 默认关闭,需要手动开启 │
└─────────────────────────────────┘
│
▼
数据库二、一级缓存(Local Cache)
特点:
- 默认开启,无法关闭
- SqlSession 级别,同一个 SqlSession 内的查询共享
- 基于 PerpetualCache(HashMap)实现
- 执行增删改、commit、close、clearCache 时失效2.1 一级缓存演示
java
// 同一个 SqlSession 内的两次相同查询,只查一次数据库
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.selectById(1L); // 查数据库,SQL 打印
User user2 = mapper.selectById(1L); // 命中缓存,不打印 SQL
System.out.println(user1 == user2); // true,同一个对象2.2 一级缓存失效场景
java
// 场景一:不同 SqlSession
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
mapper1.selectById(1L); // 查数据库
mapper2.selectById(1L); // 查数据库(不同 SqlSession,缓存不共享)
// 场景二:执行增删改
mapper1.selectById(1L); // 查数据库,放入缓存
mapper1.insert(new User()); // 增删改清空缓存
mapper1.selectById(1L); // 再次查数据库
// 场景三:手动清空缓存
mapper1.selectById(1L); // 查数据库
sqlSession.clearCache(); // 手动清空
mapper1.selectById(1L); // 再次查数据库
// 场景四:查询条件不同
mapper1.selectById(1L); // 查数据库
mapper1.selectById(2L); // 查数据库(参数不同,不走缓存)2.3 一级缓存作用域
xml
<settings>
<!-- 一级缓存作用域:SESSION(默认)/ STATEMENT -->
<setting name="localCacheScope" value="SESSION"/>
</settings>| 值 | 作用域 | 说明 |
|---|---|---|
| SESSION | SqlSession 级别 | 整个 SqlSession 生命周期内共享(默认) |
| STATEMENT | 语句级别 | 仅当前语句内共享,执行完就清除 |
三、二级缓存(Second Level Cache)
特点:
- 默认关闭,需要手动开启
- Mapper 级别(namespace 级别),多个 SqlSession 共享
- 事务提交后才会写入缓存
- 可插拔设计,支持自定义缓存实现3.1 开启二级缓存
步骤一:全局配置开启
xml
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>步骤二:Mapper XML 中添加 <cache/> 标签
xml
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache/>
<!-- 自定义缓存参数 -->
<cache
eviction="LRU" <!-- 回收策略 -->
flushInterval="60000" <!-- 刷新间隔(毫秒) -->
size="512" <!-- 最多缓存 512 个对象引用 -->
readOnly="true" <!-- 只读缓存,性能更好 -->
/>
</mapper>步骤三:实体类实现 Serializable
java
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}3.2 二级缓存演示
java
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1L); // 查数据库,SQL 打印
session1.commit(); // 提交后,数据写入二级缓存
User user2 = mapper2.selectById(1L); // 命中二级缓存,不打印 SQL
// 注意:user1 和 user2 不是同一个对象(不同 SqlSession)
// 二级缓存存的是数据,不是对象引用3.3 缓存参数
| 属性 | 说明 | 默认值 |
|---|---|---|
| eviction | 回收策略:LRU/FIFO/SOFT/WEAK | LRU |
| flushInterval | 刷新间隔(毫秒),不设置则调用时刷新 | 不设置 |
| size | 最大缓存对象数 | 1024 |
| readOnly | 只读(true=性能好,false=安全) | false |
回收策略:
LRU → 最近最少使用(默认)
FIFO → 先进先出
SOFT → 软引用(内存不足时 GC 回收)
WEAK → 弱引用(更积极回收)3.4 useCache 和 flushCache
xml
<!-- useCache:是否使用二级缓存(默认 true) -->
<select id="selectById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- flushCache:执行后是否清空缓存(默认 false,增删改默认为 true) -->
<select id="selectAll" resultType="User" flushCache="false">
SELECT * FROM user
</select>四、自定义缓存
xml
<!-- 方式一:使用第三方缓存(如 Ehcache) -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<!-- 方式二:Redis 缓存 -->
<cache type="org.mybatis.caches.redis.RedisCache"/>java
// 方式三:自定义缓存实现
public class MyCache implements Cache {
private String id;
private Map<Object, Object> cache = new ConcurrentHashMap<>();
public MyCache(String id) {
this.id = id;
}
@Override
public String getId() { return id; }
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public int getSize() {
return cache.size();
}
}五、缓存执行顺序
查询时缓存查找顺序:
① 先查二级缓存
↓ 没命中
② 再查一级缓存
↓ 没命中
③ 查数据库
↓
④ 结果放入一级缓存
↓
⑤ 事务提交后,一级缓存数据写入二级缓存六、缓存使用建议
使用建议:
① 不要滥用二级缓存
→ 只对读多写少、数据量小的表启用
→ 频繁更新的表禁用二级缓存
② 避免脏读
→ 多表关联查询时,关联表更新会导致缓存不一致
→ 单表查询才适合用二级缓存
③ 分布式环境
→ 内置缓存只在单机有效
→ 分布式环境需要使用 Redis 等集中式缓存
④ 预防内存溢出
→ 控制 size 大小
→ 避免缓存大结果集
⑤ 一级缓存无法关闭
→ 但可以设置 localCacheScope=STATEMENT 来限制作用域七、速记总结
MyBatis 两级缓存:
一级缓存:SqlSession 级别,默认开启,不可关闭
→ 同一个 SqlSession 内查询共享
→ 增删改/commit/close 后失效
二级缓存:Mapper 级别,需要手动开启
→ 同一个 namespace 的多个 SqlSession 共享
→ 实体类必须实现 Serializable
→ 事务提交后才写入缓存
缓存查找顺序:
二级 → 一级 → 数据库
速记口诀:
一级会话内,二级跨会话,
一级默认开,二级要配置,
先查二级再一级,都没有才走库。八、面试要点
| 问题 | 答案要点 |
|---|---|
| 一级缓存和二级缓存的区别? | 一级 SqlSession 级别,二级 Mapper 级别 |
| 一级缓存什么情况下失效? | 增删改、commit、close、不同 SqlSession |
| 为什么实体类要实现 Serializable? | 二级缓存需要序列化存储 |
| 分布式环境下缓存怎么处理? | 用 Redis 等集中式缓存替代内置缓存 |
