缓存机制
问题
MyBatis 的一级缓存和二级缓存是什么?它们的区别和使用注意事项?
答案
缓存体系概览
查询顺序
二级缓存 → 一级缓存 → 数据库。注意先查二级再查一级。
一级缓存(Local Cache)
| 属性 | 说明 |
|---|---|
| 作用域 | SqlSession 级别(默认) |
| 默认开启 | 是,无需配置 |
| 存储结构 | HashMap(key = statementId + params + rowBounds + SQL) |
| 生命周期 | 随 SqlSession 创建而生,关闭而亡 |
失效条件:
- 执行了
insert/update/delete(同一 SqlSession 内) - 调用了
sqlSession.clearCache() - 调用了
sqlSession.commit()或sqlSession.close() - 不同的 SqlSession
一级缓存示例
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询:走数据库
User user1 = mapper.getById(1L);
// 第二次查询:走一级缓存(同一 SqlSession,相同 SQL 和参数)
User user2 = mapper.getById(1L);
System.out.println(user1 == user2); // true(同一个对象)
// 更新操作会清空一级缓存
mapper.updateName(1L, "newName");
// 第三次查询:重新走数据库
User user3 = mapper.getById(1L);
}
Spring 中一级缓存的陷阱
在 Spring 中,默认每次 Mapper 调用都创建新的 SqlSession(通过 SqlSessionTemplate),因此一级缓存基本不生效。只有在同一个 @Transactional 事务方法内,多次相同查询才能命中一级缓存。
二级缓存(Second Level Cache)
| 属性 | 说明 |
|---|---|
| 作用域 | Mapper(namespace)级别 |
| 默认开启 | 需要手动配置 |
| 存储结构 | 序列化存储(实体类需实现 Serializable) |
| 生命周期 | 随应用存在 |
启用二级缓存:
UserMapper.xml
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache
eviction="LRU" <!-- 淘汰策略:LRU/FIFO/SOFT/WEAK -->
flushInterval="60000" <!-- 刷新间隔(毫秒) -->
size="1024" <!-- 最大缓存对象数 -->
readOnly="false" <!-- 是否只读 -->
/>
</mapper>
application.yml
mybatis:
configuration:
cache-enabled: true # 全局开关(默认 true)
二级缓存失效条件:
- 同一 namespace 下执行了
insert/update/delete - 手动清空缓存
- 设置了
flushCache="true"的 select 语句
一级缓存 vs 二级缓存
| 对比 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | Mapper namespace |
| 默认状态 | 开启 | 关闭 |
| 存储方式 | 内存对象引用 | 序列化(跨 Session 共享) |
| 线程安全 | 无需(单线程内) | 需要(多 Session 共享) |
| 实体要求 | 无 | 需实现 Serializable |
| 清空触发 | 当前 Session 增删改 | 当前 namespace 增删改 |
二级缓存的问题
多表关联查询的脏读
二级缓存按 namespace 隔离。如果 UserMapper 中有关联查询包含 Order 表数据,当 OrderMapper 更新了 Order 数据时,UserMapper 的二级缓存不会失效,导致脏数据。
解决方案:
- 关联查询不使用二级缓存(
useCache="false") - 使用
<cache-ref>让多个 namespace 共享缓存(但会降低缓存命中率) - 放弃 MyBatis 二级缓存,使用 Redis 等外部缓存
常见面试问题
Q1: MyBatis 一级缓存和二级缓存的区别?
答案:
一级缓存是 SqlSession 级别,默认开启,同一 SqlSession 内相同 SQL 直接返回缓存对象。二级缓存是 Mapper namespace 级别,需手动开启,跨 SqlSession 共享,数据以序列化形式存储。查询顺序是:二级缓存 → 一级缓存 → 数据库。
Q2: 为什么不推荐使用 MyBatis 二级缓存?
答案:
- 多表查询脏读:缓存按 namespace 隔离,关联查询可能读到过期数据
- 粒度太粗:任何增删改都清空整个 namespace 的缓存,命中率低
- 分布式场景失效:默认只是本地缓存,多实例间不共享
生产环境推荐使用 Redis 等分布式缓存替代 MyBatis 二级缓存,可以精确控制缓存粒度和失效策略。
Q3: Spring 中 MyBatis 一级缓存的表现?
答案:
Spring 默认每次 Mapper 方法调用创建新的 SqlSession,因此一级缓存几乎不生效。只有在 @Transactional 事务方法内,Spring 会复用同一个 SqlSession,此时多次相同查询才会命中一级缓存。
Q4: 如何自定义二级缓存实现?
答案:
实现 org.apache.ibatis.cache.Cache 接口,然后在 XML 中指定:
<cache type="com.example.cache.RedisCache"/>
或使用 mybatis-redis 等第三方集成包。
相关链接
- MyBatis 缓存文档
- 执行流程 - 缓存在执行流程中的位置
- Redis 基础 - 分布式缓存方案