MyBatis 自带两套缓存体系:一级缓存和二级缓存。很多接触过它的开发者大概知道一级缓存默认开启、二级缓存默认关闭,但一旦项目整合到 Spring Boot 中,缓存的表现就会出现一些和预期不一致的情况,比如事务中连续两次相同查询为什么可能不走缓存、二级缓存为什么在多表关联时容易读到脏数据。这篇文章把两套缓存的实现原理放到 Spring Boot 环境下逐一拆解,并给出实际项目中的使用建议。

一级缓存:基于 SqlSession 的本地缓存
一级缓存的实现类是 PerpetualCache,本质上就是一个 HashMap,作用域是 SqlSession 级别。也就是说,同一个 SqlSession 内,执行完全相同的查询语句(SQL 与参数都相同),第二次会直接从缓存里拿结果,不再发出 SQL。MyBatis 用 CacheKey 来判断两次查询是否相同,它由四部分信息组成:mapper 的 id、分页偏移、SQL 语句以及参数值。任何一个因素变化,CacheKey 就不同,缓存自然不会命中。
一级缓存并不是一直有效,以下几种情况会清空它:调用了 insert、update、delete 语句;执行了 sqlSession.clearCache();事务提交或回滚;或者在配置里把 localCacheScope 从 SESSION 改成了 STATEMENT。其中 STATEMENT 模式会让每次查询结束后立即清掉本地缓存,相当于禁用了一级缓存,这个配置在需要保证每次都查最新数据的场景下很有用。
// 在 MyBatis 全局配置中禁用或调整一级缓存范围
mybatis:
configuration:
local-cache-scope: statement # 默认是 session
值得注意的一点是,一级缓存存储的是对象引用,不是深拷贝。也就是说从缓存里拿回来的对象,如果你在业务代码里修改了它的属性,下次缓存命中时拿到的还是被你改过的那个对象,这种隐蔽的脏数据问题在排查时经常被忽略。
Spring Boot 整合后一级缓存的真实表现
原生 MyBatis 用法中,SqlSession 由开发者手动控制,一级缓存的边界很清晰。但整合 Spring Boot 之后,SqlSession 的生命周期交给了 Spring 管理,情况就复杂了。mybatis-spring 提供的 SqlSessionTemplate 是线程安全的,它内部通过 SqlSessionUtils.getSqlSession() 获取会话:如果当前线程没有绑定事务,就每次新建一个 SqlSession,用完立即关闭,一级缓存形同虚设;如果方法运行在 @Transactional 事务中,同一个事务内会复用同一个 SqlSession,一级缓存才真正生效。
这就解释了一个常见困惑:方法 A 里连续两次相同查询,没加事务注解时控制台打印两条 SQL,加了 @Transactional 后只打印一条。不是缓存坏了,而是缓存的作用域变了。所以严格来说,Spring Boot 下的一级缓存应该理解为事务级缓存,而不是会话级缓存。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 没有事务:两次查询各执行一条 SQL,一级缓存不生效
public User queryWithoutTx(Long id) {
userMapper.selectById(id);
return userMapper.selectById(id);
}
// 有事务:两次查询复用同一个 SqlSession,只执行一条 SQL
@Transactional
public User queryWithTx(Long id) {
userMapper.selectById(id);
return userMapper.selectById(id);
}
}
另一个容易踩坑的地方是嵌套事务或者事务传播行为变化时,SqlSession 的绑定关系会跟着变,缓存命中范围也随之改变。排查这类问题时,可以开启 SQL 日志观察实际执行的语句条数,这是判断一级缓存是否生效最直接的手段。
二级缓存:namespace 级别的跨会话缓存
二级缓存的作用域是 mapper 的 namespace,多个 SqlSession 可以共享同一份缓存数据。开启它需要两步:在全局配置里把 cache-enabled 设为 true(默认就是 true),再在对应的 mapper XML 中加一行 <cache/> 标签,或者在使用注解的 mapper 接口上标注 @CacheNamespace。开启后,查询结果会先存到一级缓存,事务提交时再放进二级缓存。
<mapper namespace="com.example.mapper.UserMapper">
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
<select id="selectById" resultType="User">
select * from user where id = #{id}
</select>
</mapper>
二级缓存最典型的问题是跨 namespace 的脏数据。因为缓存按 namespace 隔离,如果 UserMapper 的查询里关联了订单表,而 OrderMapper 执行了更新操作,只会清空 OrderMapper 自己的缓存,UserMapper 缓存里的关联结果不会被刷新,下次查询拿到的就是过期数据。官方也意识到了这个问题,提供了 <cache-ref> 标签让多个 namespace 共享同一份缓存,但这只是缓解手段,一旦 namespace 之间共享缓存,缓存的粒度又变得混乱,维护成本不低。
另外,二级缓存要求实体类实现 Serializable 接口,因为缓存写入时默认要做序列化。把 readOnly 设为 true 可以返回缓存对象的同一实例(省去反序列化开销),但代价是调用方拿到的是共享引用,任何修改都会污染缓存,只适合确认查询结果只读的场景。
实际项目中的取舍建议
单机小型项目,如果查询多、更新少、表之间没有复杂的关联更新关系,二级缓存可以带来一定收益。但只要存在多表关联更新,或者应用要部署多个实例做负载均衡,MyBatis 自带的二级缓存就不适用了:它是本地缓存,节点之间无法同步,各实例的缓存数据会各自为政,脏数据几乎不可避免。
分布式或者多实例场景下,更稳妥的做法是关闭二级缓存,改用集中式缓存方案。在 Spring Boot 里引入 Redis 或者 Caffeine 做业务层缓存,通过 @Cacheable 注解配合 Spring Cache 抽象来管理,缓存的粒度、过期策略、更新逻辑都由业务代码显式控制,排查问题也远比藏在框架内部的 MyBatis 缓存透明。
@Service
public class UserService {
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User selectById(Long id) {
return userMapper.selectById(id);
}
@CacheEvict(value = "user", key = "#id")
public void updateUser(User user) {
userMapper.updateById(user);
}
}
总结一下:一级缓存在 Spring Boot 下等于事务级缓存,默认开启,一般不用干预;二级缓存是 namespace 级别的本地缓存,多表关联和分布式部署场景下风险大于收益,大多数项目直接关闭、交给专门的缓存中间件处理是更常见也更安全的选择。理解这两套缓存的真实作用域,比纠结要不要开启它们更重要。
MyBatis缓存Spring Boot一级缓存修改时间:2026-09-12 17:20:34