好的,我来帮你系统、详细地讲解 MyBatis 两级缓存(一级缓存 + 二级缓存)可能导致的问题,包括原因、场景、解决方案,并带示例说明。内容会尽量专业且实用。
MyBatis 两级缓存可能导致的问题详解
MyBatis 的缓存分为:
- 一级缓存(SqlSession 级别)
- 默认开启,不可关闭
- 生命周期:
SqlSession生命周期内 - 数据隔离:每个
SqlSession独立缓存
- 二级缓存(Mapper 级别,全局共享缓存)
- 默认关闭,需要在 mapper.xml 或 Mapper 接口上开启
<cache/> - 生命周期:应用程序运行期间
- 数据隔离:同一个 namespace(Mapper)共享缓存
- 默认关闭,需要在 mapper.xml 或 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 中操作:
- Session1 查询用户 1 → 数据缓存到二级缓存
- Session2 更新用户 1
- Session1 再次查询 → 读取二级缓存 → 得到旧数据
原因:二级缓存不会自动感知数据库变化,除非 MyBatis 执行了 增删改操作清除缓存(默认只清除本 Mapper 缓存)。
解决方案:
- 使用
<cache/>配置flushInterval或readWrite="true"控制刷新周期 - 保证更新操作在 MyBatis 中走 Mapper,否则外部修改会导致缓存脏数据
- 高并发场景可以考虑缓存失效策略或二级缓存禁用
<cache
eviction="LRU"
flushInterval="60000" <!-- 60秒自动刷新 -->
size="512"
readOnly="false"/>
2.2 数据一致性问题
- 二级缓存是 Mapper 级别的,不同 Mapper namespace 数据修改可能无法及时同步缓存
- 跨表更新,比如
Order表更新影响User表统计,也可能导致缓存不一致
解决方案:
- 对复杂业务使用 手动清理缓存:
sqlSession.getConfiguration()
.getCache(UserMapper.class.getName())
.clear();
- 或者禁用二级缓存,对于频繁更新的数据表不使用缓存
2.3 高并发下的缓存击穿 / 并发问题
- 高并发查询同一个数据,二级缓存可能被多个线程同时更新
- 默认 MyBatis 二级缓存基于
HashMap或ConcurrentHashMap实现,但更新不完全原子 - 会出现重复查询数据库、缓存竞争
解决方案:
- 使用 Redis 等外部缓存做分布式缓存
- MyBatis 2.x 可以使用第三方 Cache 实现,例如
MyBatis-Redis-Cache - 配合分布式锁或同步策略保证高并发安全
2.4 内存占用问题
- 二级缓存对象存储在内存中(默认 PerpetualCache + LRU)
- 大对象或海量查询可能导致 JVM 堆内存压力
解决方案:
- 设置缓存容量和过期策略:
<cache eviction="LRU" size="1000" flushInterval="300000"/>
- 对大数据量表禁用二级缓存
2.5 缓存更新粒度过粗
- MyBatis 二级缓存默认 Mapper namespace 级别
- 一个表的所有查询共享缓存,更新某个记录会清空整个 Mapper 缓存
- 可能导致大量缓存失效,降低命中率
解决方案:
- 对热点查询使用单独 Mapper
- 使用二级缓存插件支持更细粒度缓存,例如基于 Redis 的
MyBatis Redis Cache
3. 总结与建议
| 缓存级别 | 主要问题 | 建议 |
|---|---|---|
| 一级缓存 | 脏数据、生命周期过长 | SqlSession 尽量短生命周期;手动 clearCache() |
| 二级缓存 | 脏数据、并发问题、内存占用、缓存颗粒粗 | 配置缓存刷新策略、限制大小、读写模式;高并发或复杂业务可考虑外部缓存 |
4. 小结
- 一级缓存:安全、默认开启,但注意 SqlSession 生命周期
- 二级缓存:提高性能,但易产生脏数据和一致性问题
- 对于 频繁更新或跨表操作的表,建议 禁用二级缓存 或使用 分布式缓存
- 对于 只读或少更新的热点数据表,二级缓存可以显著提升性能