跳到主要内容

缓存机制

问题

MyBatis 的一级缓存和二级缓存是什么?它们的区别和使用注意事项?

答案

缓存体系概览

查询顺序

二级缓存 → 一级缓存 → 数据库。注意先查二级再查一级。

一级缓存(Local Cache)

属性说明
作用域SqlSession 级别(默认)
默认开启是,无需配置
存储结构HashMap(key = statementId + params + rowBounds + SQL)
生命周期随 SqlSession 创建而生,关闭而亡

失效条件

  1. 执行了 insert/update/delete(同一 SqlSession 内)
  2. 调用了 sqlSession.clearCache()
  3. 调用了 sqlSession.commit()sqlSession.close()
  4. 不同的 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)

二级缓存失效条件

  1. 同一 namespace 下执行了 insert/update/delete
  2. 手动清空缓存
  3. 设置了 flushCache="true" 的 select 语句

一级缓存 vs 二级缓存

对比一级缓存二级缓存
作用域SqlSessionMapper namespace
默认状态开启关闭
存储方式内存对象引用序列化(跨 Session 共享)
线程安全无需(单线程内)需要(多 Session 共享)
实体要求需实现 Serializable
清空触发当前 Session 增删改当前 namespace 增删改

二级缓存的问题

多表关联查询的脏读

二级缓存按 namespace 隔离。如果 UserMapper 中有关联查询包含 Order 表数据,当 OrderMapper 更新了 Order 数据时,UserMapper 的二级缓存不会失效,导致脏数据

解决方案:

  1. 关联查询不使用二级缓存(useCache="false"
  2. 使用 <cache-ref> 让多个 namespace 共享缓存(但会降低缓存命中率)
  3. 放弃 MyBatis 二级缓存,使用 Redis 等外部缓存

常见面试问题

Q1: MyBatis 一级缓存和二级缓存的区别?

答案

一级缓存是 SqlSession 级别,默认开启,同一 SqlSession 内相同 SQL 直接返回缓存对象。二级缓存是 Mapper namespace 级别,需手动开启,跨 SqlSession 共享,数据以序列化形式存储。查询顺序是:二级缓存 → 一级缓存 → 数据库。

Q2: 为什么不推荐使用 MyBatis 二级缓存?

答案

  1. 多表查询脏读:缓存按 namespace 隔离,关联查询可能读到过期数据
  2. 粒度太粗:任何增删改都清空整个 namespace 的缓存,命中率低
  3. 分布式场景失效:默认只是本地缓存,多实例间不共享

生产环境推荐使用 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 等第三方集成包。

相关链接