Mybatis与Hibernate缓存的具体使用
一、Mybatis与Hibernate缓存对比
1、Hibernate 缓存机制
Hibernate 的缓存体系分为一级缓存和二级缓存两层,核心作用是减少对数据库的直接访问,大幅提升数据查询性能。
- 一级缓存(Session 级缓存):它是 Hibernate 内置且默认开启的缓存,无法手动关闭,生命周期与 Session 完全绑定,仅在当前事务内生效。同一 Session 中执行相同 ID 的实体查询时,只会发起一次数据库访问,后续请求直接从内存缓存中返回对象,避免重复 SQL 执行。当调用
session.clear()、session.close()或事务结束时,一级缓存会被清空。 - 二级缓存(SessionFactory 级缓存):属于全局共享缓存,可跨多个 Session、甚至跨应用实例共享数据,默认不启用,需要手动配置第三方缓存插件(如 Hibernate 默认支持的 Ehcache)才能开启。它仅适合存储修改频率低、非核心的常量类数据,对于高频更新、财务类等对一致性要求极高的数据,禁止放入二级缓存,避免出现数据不一致问题。二级缓存支持事务型、读写型等4种并发访问策略,可根据业务场景匹配不同的事务隔离级别,保障缓存数据的安全性。
- 额外特性:Hibernate 还配套提供 Query 缓存,可对 HQL 查询结果进行缓存,进一步优化批量检索场景的性能。
2、MyBatis 缓存机制
MyBatis 同样设计了两级缓存体系,但整体实现更轻量化,灵活性更高,同时对开发者的透明度更低。
- 一级缓存(SqlSession 级缓存):默认自动开启,作用范围是当前 SqlSession,同一会话内执行相同参数的 Mapper 查询时,会直接返回缓存结果,避免重复查库。当 SqlSession 执行任意一次增删改操作、调用
close()或clearCache()方法时,一级缓存会被强制清空。 - 二级缓存(Mapper 级缓存):作用范围是同一个 Mapper 的命名空间,可跨多个 SqlSession 共享数据,默认关闭,需要手动在配置文件中开启。开启后对应的 Mapper 下所有查询语句会被缓存,增删改操作会自动清空该命名空间下的所有缓存数据。
- 核心特点:MyBatis 缓存仅做简单的对象引用存储,不会像 Hibernate 那样自动维护缓存与数据库的一致性,当存在多表关联查询、多命名空间交叉更新的场景时,极易出现脏数据,且框架本身不会给出任何异常提示,需要开发者手动控制缓存的失效逻辑。
3、两者核心差异对比
| 对比维度 | Hibernate 缓存 | MyBatis 缓存 |
|---|---|---|
| 自动化程度 | 高,框架自动维护缓存生命周期与数据同步 | 低,需开发者手动控制缓存的更新与失效 |
| 数据一致性保障 | 内置完善的并发策略,出现脏数据会主动提示 | 无自动校验,脏数据无任何告警,风险更高 |
| 缓存适配场景 | 适配绝大多数业务场景,对多表关联查询支持友好 | 更适合简单单表查询,复杂关联场景易出现一致性问题 |
| 配置复杂度 | 二级缓存需额外配置插件与并发策略,上手门槛略高 | 配置简单,仅需少量标签即可开启二级缓存 |
二、缓存常见错误与陷阱
1、MyBatis 缓存常见错误与陷阱
MyBatis 的缓存机制相对“轻量”且“透明”,这导致开发者往往忽略其底层实现细节,从而引发隐蔽的 Bug。
- 一级缓存导致的“脏读”与线程安全问题
错误场景:在 Web 应用中,错误地将SqlSession定义为类的成员变量或静态变量,导致多个请求线程共享同一个SqlSession。
后果:
- 线程不安全:MyBatis 一级缓存底层是
HashMap,无同步控制。多线程并发读写会导致数据错乱。 - 脏读风险:线程 A 修改了数据但未提交,线程 B 通过共享的 SqlSession 查询同一 ID,可能读到线程 A 缓存中的旧数据或中间状态数据。
// ❌ 错误示范:SqlSession 作为成员变量,被多线程共享
public class UserService {
private SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
public User getUserById(int id) {
// 高并发下,不同线程可能读取到其他线程未提交的缓存数据
return sqlSession.selectOne("selectUser", id);
}
}
正确做法:确保每个线程拥有独立的 SqlSession,通常在 Service 层通过 Spring 管理事务,或使用 try-with-resources 确保 Session 随请求结束而关闭。
- Spring 事务中一级缓存失效或累积导致 OOM
错误场景:
- 场景 A(失效):在同一个事务中,先执行更新操作,再执行查询。虽然理论上 MyBatis 会在更新时清空一级缓存,但如果使用了特定的传播行为(如
REQUIRES_NEW),会创建新的 SqlSession,导致前一个 Session 的缓存不可见,看似“失效”。 - 场景 B(OOM):在长事务或大循环中频繁查询不同 ID 的数据,且未手动清理缓存。由于一级缓存生命周期绑定 SqlSession,只要 Session 不关闭,缓存会一直累积,最终导致堆内存溢出。
// ❌ 错误示范:长事务中大量查询不同ID,导致一级缓存无限膨胀
@Transactional
public void processLargeData() {
for (int i = 0; i < 100000; i++) {
// 每次查询不同ID,结果都会存入当前SqlSession的一级缓存
orderMapper.selectById(i);
}
// 事务提交前,这10万个对象一直占用内存
}
正确做法:
- 对于批量处理,建议禁用一级缓存(设置
local-cache-scope: STATEMENT)。 - 或在循环中定期调用
sqlSession.clearCache()。
- 二级缓存的“序列化”与“多表关联”陷阱
错误场景:
- 序列化异常:开启二级缓存(尤其是集成 Redis/Ehcache)时,实体类未实现
Serializable接口,或者使用了不支持序列化的字段类型,导致缓存写入/读取失败。 - 多表关联脏数据:MyBatis 二级缓存是基于 Mapper Namespace 的。如果
OrderMapper缓存了订单信息,而ItemMapper更新了订单项并触发了ItemMapper的缓存清空,但OrderMapper的缓存并未感知到变化。此时查询订单,会得到包含旧订单项的脏数据。
<!-- OrderMapper.xml -->
<mapper namespace="com.example.OrderMapper">
<cache/> <!-- 开启二级缓存 -->
<select id="getOrderWithItems" resultType="Order">
SELECT * FROM orders o JOIN items i ON o.id = i.order_id WHERE o.id = {id}
</select>
</mapper>
<!-- ItemMapper.xml -->
<mapper namespace="com.example.ItemMapper">
<cache/>
<update id="updateItem">
UPDATE items SET name = {name} WHERE id = {id}
</update>
</mapper>后果:更新 Item 后,OrderMapper 中的联合查询结果仍然是旧的,因为两个 Mapper 的缓存空间是隔离的。
正确做法:
- 避免在二级缓存中使用复杂的多表关联查询。
- 使用
<cache-ref>让多个 Mapper 共享同一个缓存空间,但这会降低缓存命中率。 - 最佳实践:在分布式或多服务架构中,彻底禁用 MyBatis 二级缓存,改用 Redis 等外部集中式缓存,由业务代码控制缓存一致性。
- 微服务架构下的一级缓存误导
错误场景:在微服务环境中,默认开启一级缓存。如果服务实例 A 查询了数据,随后服务实例 B(或同一实例的其他事务)修改了数据库,实例 A 的后续查询仍可能命中本地一级缓存(如果 SqlSession 未正确关闭或复用),导致数据不一致。
建议:在application.yml中显式配置local-cache-scope: STATEMENT以禁用一级缓存,依赖外部缓存或数据库实时性。
2、Hibernate 缓存常见错误与陷阱
Hibernate 缓存更智能,但也更复杂。错误通常源于对“对象状态”和“缓存策略”的误解。
- 二级缓存策略选择不当导致数据不一致
错误场景:对高频更新的数据(如库存、账户余额)使用了read-only或nonstrict-read-write策略,甚至在集群环境下未配置正确的并发锁机制。
后果:
- 脏数据:节点 A 更新了数据并更新了缓存,节点 B 的缓存未及时失效或更新,导致用户读到旧值。
- 异常抛出:如果使用
read-only策略却尝试更新实体,Hibernate 会直接抛出异常。
// ❌ 错误配置:对频繁变动的财务数据使用只读缓存
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY) // 严禁用于可写数据
public class AccountBalance { ... }
正确做法:
- 仅将极少修改、非核心、允许短暂不一致的数据(如字典表、配置项)放入二级缓存。
- 对于强一致性要求的数据,禁止使用二级缓存,或采用
transactional策略(需 JTA 支持)。
- Query Cache 与 二级缓存配合失误
错误场景:启用了 Query Cache (hibernate.cache.use_query_cache=true),但未启用二级缓存,或者查询条件中包含动态参数但未正确理解缓存 Key 的生成规则。
原理:Query Cache 存储的是 查询结果集的 ID 列表,而不是实体对象本身。实体对象仍需从二级缓存或数据库加载。
后果:
- 如果二级缓存未启用,Query Cache 命中后,Hibernate 仍需根据 ID 逐个去数据库加载实体,性能提升有限,反而增加了内存开销。
- 如果表数据更新,Query Cache 会自动失效,但若依赖 Query Cache 优化高频相同条件查询,需注意其失效频率。
// ❌ 错误用法:期望 Query Cache 能缓存整个对象图
Query query = session.createQuery("from User u where u.age > :age");
query.setParameter("age", 18);
query.setCacheable(true); // 仅缓存了 ID 列表
List<User> users = query.list();
// 如果二级缓存没开,这里会根据 ID 再次查库
- Session 管理不当导致一级缓存内存泄漏
错误场景:在一个长生命周期的 Session 中执行大量的load()或list()操作,且不调用session.clear()。
后果:Hibernate 一级缓存(Session 级)会持有所有加载过的持久化对象引用。随着操作增多,JVM 堆内存被占满,触发 Full GC 甚至 OOM。
典型场景:批量导入/导出几万条数据时,使用同一个 Session。
// ❌ 错误示范:批量处理未清理 Session 缓存
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
for (int i = 0; i < 50000; i++) {
User user = session.get(User.class, i); // 每个对象都进入一级缓存
user.setName("Updated");
if (i % 50 == 0) {
session.flush();
session.clear(); // ✅ 必须定期清理,否则 OOM
}
}
tx.commit();
session.close();
load()与get()混淆引发的 LazyInitializationException
错误场景:使用session.load()获取对象,该对象是一个代理对象(Proxy)。如果在 Session 关闭后访问其非 ID 属性,会抛出异常。
原因:load()优先从缓存查找,若缓存无则返回代理对象,不立即查库。若后续访问属性时 Session 已关闭,无法初始化代理。
对比:get()总是立即查库(若缓存无),返回真实对象或 null。
User user = session.load(User.class, 1L); // 返回代理对象,不查库 session.close(); System.out.println(user.getName()); // ❌ 抛出 LazyInitializationException
3、总结与最佳实践建议
| 维度 | MyBatis 避坑指南 | Hibernate 避坑指南 |
|---|---|---|
| 一级缓存 | 微服务/分布式环境下建议禁用 (STATEMENT 模式),避免脏读和内存累积。确保 SqlSession 线程隔离。 | 严格管理 Session 生命周期。批量操作务必定期 clear()。理解 load vs get 的区别。 |
| 二级缓存 | 慎用。多表关联 易出脏数据。建议仅在单表、读多写少场景使用,或直接交由 Redis 处理。 | 精选数据。仅缓存不变或极少变的字典/配置数据。避免缓存金融/库存等强一致性数据。 |
| 一致性 | 框架不保证跨 Mapper 的一致性。需开发者手动维护或通过外部缓存解决。 | 框架提供较好的并发策略,但配置复杂。需根据事务隔离级别选择合适的 CacheConcurrencyStrategy。 |
| 调试 | 开启 SQL 日志,观察是否重复执行相同 SQL。监控缓存命中率。 | 开启 Hibernate Statistics,监控 L1/L2 Cache Hit/Miss 比例,识别内存泄漏。 |
核心原则:
- 不要过度依赖框架内置缓存来解决高性能问题,优先考虑业务层面的缓存设计(如 Redis)。
- 一致性优于性能:在涉及金钱、库存、状态流转的场景,宁可牺牲性能查库,也不要冒险使用可能导致脏数据的缓存策略。
- 监控与测试:上线前必须进行压力测试,监控 JVM 内存使用和缓存命中率,及时发现缓存失效或泄漏问题。
到此这篇关于Mybatis与Hibernate缓存的具体使用的文章就介绍到这了,更多相关Mybatis与Hibernate缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Java String类简单用法实战示例【字符串输出、比较】
这篇文章主要介绍了Java String类简单用法,结合具体实例形式分析了Java使用String类实现字符串的输出和比较功能相关操作技巧,需要的朋友可以参考下2019-07-07


最新评论