好的,我来帮你系统、详细地讲解 MyBatis 两级缓存(一级缓存 + 二级缓存)可能导致的问题,包括原因、场景、解决方案,并带示例说明。内容会尽量专业且实用。


MyBatis 两级缓存可能导致的问题详解

MyBatis 的缓存分为:

  1. 一级缓存(SqlSession 级别)
    • 默认开启,不可关闭
    • 生命周期:SqlSession 生命周期内
    • 数据隔离:每个 SqlSession 独立缓存
  2. 二级缓存(Mapper 级别,全局共享缓存)
    • 默认关闭,需要在 mapper.xml 或 Mapper 接口上开启 <cache/>
    • 生命周期:应用程序运行期间
    • 数据隔离:同一个 namespace(Mapper)共享缓存

MyBatis 缓存可以显著提高查询性能,但同时也可能引发数据不一致、脏数据、缓存污染等问题。


1. 一级缓存可能导致的问题

1.1 数据不一致

场景:同一个 SqlSession 中先查询再更新,再查询可能得到旧数据。

SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);

// 查询用户信息
User user1 = mapper.getUserById(1); // 缓存 user1
// 修改数据库中该用户的名称(通过另一个 SqlSession 或直接 SQL 更新)
mapper.updateUserName(1, "新的名字");

// 再次查询
User user2 = mapper.getUserById(1);

问题user2 可能还是旧数据,因为一级缓存会直接返回 user1

解决方案

  • 更新/删除操作后,MyBatis 会清空一级缓存 (默认行为)
  • 如果手动修改了数据库,或有外部更新,一级缓存不会感知,需要手动清除:
session.clearCache(); // 清空一级缓存


1.2 SqlSession 生命周期不当

如果你在 长生命周期的 SqlSession 中频繁查询:

  • 一级缓存会积累大量对象
  • 内存占用增加
  • 可能导致 OOM(Out Of Memory)

建议

  • SqlSession 尽量短生命周期,完成一组操作就关闭
  • 使用连接池(如 DBCP、HikariCP)管理 SqlSession

2. 二级缓存可能导致的问题

二级缓存是 跨 SqlSession、同一个 Mapper namespace 的共享缓存,所以问题更复杂。

2.1 脏数据(Stale Data)

场景:用户表 User 在多个 SqlSession 中操作:

  1. Session1 查询用户 1 → 数据缓存到二级缓存
  2. Session2 更新用户 1
  3. Session1 再次查询 → 读取二级缓存 → 得到旧数据

原因:二级缓存不会自动感知数据库变化,除非 MyBatis 执行了 增删改操作清除缓存(默认只清除本 Mapper 缓存)。

解决方案

  • 使用 <cache/> 配置 flushIntervalreadWrite="true" 控制刷新周期
  • 保证更新操作在 MyBatis 中走 Mapper,否则外部修改会导致缓存脏数据
  • 高并发场景可以考虑缓存失效策略或二级缓存禁用
&lt;cache 
    eviction="LRU" 
    flushInterval="60000"  &lt;!-- 60秒自动刷新 -->
    size="512" 
    readOnly="false"/>


2.2 数据一致性问题

  • 二级缓存是 Mapper 级别的,不同 Mapper namespace 数据修改可能无法及时同步缓存
  • 跨表更新,比如 Order 表更新影响 User 表统计,也可能导致缓存不一致

解决方案

  • 对复杂业务使用 手动清理缓存
sqlSession.getConfiguration()
    .getCache(UserMapper.class.getName())
    .clear();

  • 或者禁用二级缓存,对于频繁更新的数据表不使用缓存

2.3 高并发下的缓存击穿 / 并发问题

  • 高并发查询同一个数据,二级缓存可能被多个线程同时更新
  • 默认 MyBatis 二级缓存基于 HashMapConcurrentHashMap 实现,但更新不完全原子
  • 会出现重复查询数据库、缓存竞争

解决方案

  • 使用 Redis 等外部缓存做分布式缓存
  • MyBatis 2.x 可以使用第三方 Cache 实现,例如 MyBatis-Redis-Cache
  • 配合分布式锁或同步策略保证高并发安全

2.4 内存占用问题

  • 二级缓存对象存储在内存中(默认 PerpetualCache + LRU)
  • 大对象或海量查询可能导致 JVM 堆内存压力

解决方案

  • 设置缓存容量和过期策略:
&lt;cache eviction="LRU" size="1000" flushInterval="300000"/>

  • 对大数据量表禁用二级缓存

2.5 缓存更新粒度过粗

  • MyBatis 二级缓存默认 Mapper namespace 级别
  • 一个表的所有查询共享缓存,更新某个记录会清空整个 Mapper 缓存
  • 可能导致大量缓存失效,降低命中率

解决方案

  • 对热点查询使用单独 Mapper
  • 使用二级缓存插件支持更细粒度缓存,例如基于 Redis 的 MyBatis Redis Cache

3. 总结与建议

缓存级别主要问题建议
一级缓存脏数据、生命周期过长SqlSession 尽量短生命周期;手动 clearCache()
二级缓存脏数据、并发问题、内存占用、缓存颗粒粗配置缓存刷新策略、限制大小、读写模式;高并发或复杂业务可考虑外部缓存

4. 小结

  • 一级缓存:安全、默认开启,但注意 SqlSession 生命周期
  • 二级缓存:提高性能,但易产生脏数据和一致性问题
  • 对于 频繁更新或跨表操作的表,建议 禁用二级缓存 或使用 分布式缓存
  • 对于 只读或少更新的热点数据表,二级缓存可以显著提升性能