1. 概述
ORM框架(如Hibernate)的核心优势之一是能透明缓存从数据库获取的数据。这种机制能有效减少高频数据的数据库访问开销,尤其对于包含复杂对象图的实体,当读写比例较高时,性能提升会非常显著。
本文将深入探讨Hibernate二级缓存机制。我们将通过简洁的示例解释核心概念,主要使用JPA标准API,仅在必要时回退到Hibernate原生API。
2. 什么是二级缓存?
Hibernate自带一级缓存(Session级别缓存),确保同一持久化上下文中每个实体实例只加载一次。当Session关闭时,一级缓存随之销毁——这种设计很合理,毕竟不同Session需要隔离操作。
而二级缓存是SessionFactory级别的缓存,所有由同一SessionFactory创建的Session共享该缓存。当通过ID查找实体时(无论是业务代码还是Hibernate内部操作),若该实体启用了二级缓存,会按以下顺序处理:
- ✅ 先查一级缓存,命中则直接返回
- ⚠️ 一级缓存未命中时,检查二级缓存
- 若二级缓存存在实体状态数据,则组装实例返回
- ❌ 两级缓存均未命中时,从数据库加载数据并组装实例
一旦实体存入持久化上下文(一级缓存),在Session关闭或手动驱逐前,后续查找都会直接返回该实例。同时,新加载的实体状态也会同步存入二级缓存(如果尚未存在)。
3. 区域工厂(Region Factory)
Hibernate二级缓存设计上与具体缓存提供商解耦,只需实现org.hibernate.cache.spi.RegionFactory
接口作为桥梁。在Hibernate 5.x时代,官方曾直接提供Ehcache、Infinispan等实现。
⚠️ 重要变化:从6.x版本开始,标准接入方式改为通过JSR-107 (JCache)适配器。这使Hibernate团队无需为每个缓存提供商单独实现RegionFactory。
本文以Ehcache为例,需添加以下Maven依赖:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jcache</artifactId>
<version>6.5.2.Final</version>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.10.8</version>
<classifier>jakarta</classifier>
</dependency>
📌 关键点:
hibernate-jcache
会传递引入匹配的hibernate-core
版本。Ehcache依赖必须使用jakarta
分类器以兼容Hibernate 6.x。
4. 启用二级缓存
通过以下四个属性启用二级缓存并指定Ehcache配置:
hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.jcache.internal.JCacheRegionFactory
hibernate.javax.cache.uri=ehcache.xml
hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider
在persistence.xml
中的配置示例:
<properties>
...
<property name="hibernate.cache.use_second_level_cache" value="true" />
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.jcache.internal.JCacheRegionFactory" />
<property name="hibernate.javax.cache.uri" value="ehcache.xml" />
<property name="hibernate.javax.cache.provider" value="org.ehcache.jsr107.EhcacheCachingProvider" />
...
</properties>
📌 调试技巧:将
hibernate.cache.use_second_level_cache
设为false
可快速禁用缓存。hibernate.javax.cache.uri
指定Ehcache配置文件路径(无协议时默认从类路径加载)。
5. 使实体可缓存
要启用实体缓存,需添加两个注解:
@Entity
@Cacheable // 标准JPA注解(非必需但推荐)
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // Hibernate专用注解
public class Foo {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "ID")
private long id;
@Column(name = "NAME")
private String name;
// getters and setters
}
每个实体类使用独立缓存区域,默认区域名为全限定类名(如com.baeldung.hibernate.cache.model.Foo
)。可通过显式设置区域名实现多实体共享缓存,但不建议将版本化与非版本化实体混用同一区域。
验证缓存是否生效的测试代码:
@Autowired
EntityManagerFactory emf;
var cache = emf.getCache();
Foo foo = new Foo();
fooService.create(foo);
fooService.findOne(foo.getId());
var wasCached = cache.contains(Foo.class, foo.getId());
assertTrue(wasCached);
📌 验证方法:开启Hibernate SQL日志,多次调用
findOne()
,观察select
语句是否只打印一次(首次从数据库加载,后续从缓存获取)。
6. 缓存并发策略
根据业务场景选择合适的并发策略:
策略 | 适用场景 | 特点 |
---|---|---|
READ_ONLY |
永不修改的实体(如静态参考数据) | 最高性能,修改时抛异常 |
NONSTRICT_READ_WRITE |
可容忍最终一致性的场景 | 事务提交后更新缓存,存在短暂数据不一致窗口 |
READ_WRITE |
需要强一致性的场景 | 使用"软锁"保证一致性,更新时加锁,事务提交后释放 |
TRANSACTIONAL |
分布式XA事务环境 | 缓存操作与数据库事务在XA中同步提交/回滚 |
7. 缓存管理
未配置过期/驱逐策略时,缓存可能无限增长耗尽内存。Hibernate将这类管理职责交给缓存提供商,通过配置文件控制。
Ehcache配置示例(限制Foo
实体最大缓存1000条):
<cache-template name="entities">
<resources>
<heap unit="entries">1000</heap>
</resources>
</cache-template>
<cache
alias="com.baeldung.hibernate.cache.model.Foo"
uses-template="entities">
</cache>
⚠️ 迁移注意事项:
- 默认自动创建未声明的缓存区域
- 设置
hibernate.javax.cache.missing_cache_strategy=fail
可禁用自动创建(推荐用于精细内存控制) - 旧版
org.hibernate.cache.UpdateTimestampsCache
和StandardQueryCache
已更名为default-update-timestamps-region
和default-query-results-region
8. 集合缓存
集合默认不缓存,需显式标记:
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Foo {
...
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@OneToMany
private Collection<Bar> bars;
// getters and setters
}
9. 缓存状态的内部表示
实体在二级缓存中通常以分解状态存储,而非Java对象实例:
- ✅ ID(主键)不存储(作为缓存键的一部分)
- ❌ 瞬态属性不存储
- ❌ 集合不单独存储(见下节)
- ✅ 非关联属性值按原样存储
- ✅
ToOne
关联仅存储外键ID
这种设计空间效率高,且易于与数据库同步。特殊情况:满足以下条件时,只读实体可存储为直接引用:
- 实体类标记
@Immutable
- 仅包含标量字段(无关联)
- 设置
hibernate.cache.use_reference_entries=true
📌 性能提示:直接引用查找性能更高,但仅限纯内存缓存(有复制/磁盘存储时不可用)。
9.1 集合的内部表示
集合存储在独立缓存区域(区域名=类名+属性名,如com.baeldung.hibernate.cache.model.Foo.bars
)。每个集合条目仅缓存包含实体的ID,因此建议同时启用关联实体的二级缓存。
10. HQL DML与原生查询的缓存失效
HQL DML操作(如update/delete
)能自动识别受影响实体:
entityManager.createQuery("update Foo set … where …").executeUpdate();
→ 仅驱逐所有Foo
实例,其他缓存不受影响。
原生SQL DML操作无法识别影响范围,会清空整个二级缓存:
session.createNativeQuery("update ROO set … where …").executeUpdate();
→ ❌ 全区域失效(通常非预期行为)
解决方案:通过Hibernate原生API显式声明受影响实体:
Query nativeQuery = entityManager.createNativeQuery("update FOO set ... where ...");
nativeQuery.unwrap(org.hibernate.query.NativeQuery.class)
.addSynchronizedEntityClass(Foo.class);
nativeQuery.executeUpdate();
📌 注意:仅DML语句(
insert/update/delete
和过程调用)会触发失效,原生select
查询不影响缓存。
11. 查询缓存
对频繁执行且结果集变化少的HQL查询,可缓存结果集。启用方式:
hibernate.cache.use_query_cache=true
通过查询Hint标记可缓存查询:
entityManager.createQuery("select f from Foo f")
.setHint("org.hibernate.cacheable", true)
.getResultList();
11.1 查询缓存最佳实践
关键注意事项:
- ✅ 仅缓存返回实体的ID,强烈建议同时启用这些实体的二级缓存
- ❌ 参数组合多的查询(如分页查询)不适合缓存(每个参数组合生成独立缓存条目)
- ❌ 涉及高频修改实体的查询不适合缓存(任何相关实体修改都会使缓存失效)
- ✅ 默认使用
default-query-results-region
,可通过自定义区域名实现差异化配置 - ⚠️ 特殊区域:
default-update-timestamps-region
存储查询表的最后更新时间戳,用于验证缓存结果有效性。该区域必须禁用自动过期/驱逐:
<cache alias="default-update-timestamps-region">
<expiry>
<none />
</expiry>
<resources>
<heap unit="entries">1000</heap>
</resources>
</cache>
12. 总结
Hibernate二级缓存通过简单配置即可透明提升应用性能,核心要点:
- 正确选择并发策略(
READ_WRITE
适合多数场景) - 合理配置缓存区域和过期策略
- 注意集合缓存和查询缓存的特殊性
- 避免原生DML查询的缓存踩坑
完整实现代码见GitHub仓库。