1. 概述

ORM框架(如Hibernate)的核心优势之一是能透明缓存从数据库获取的数据。这种机制能有效减少高频数据的数据库访问开销,尤其对于包含复杂对象图的实体,当读写比例较高时,性能提升会非常显著。

本文将深入探讨Hibernate二级缓存机制。我们将通过简洁的示例解释核心概念,主要使用JPA标准API,仅在必要时回退到Hibernate原生API。

2. 什么是二级缓存?

Hibernate自带一级缓存(Session级别缓存),确保同一持久化上下文中每个实体实例只加载一次。当Session关闭时,一级缓存随之销毁——这种设计很合理,毕竟不同Session需要隔离操作。

二级缓存是SessionFactory级别的缓存,所有由同一SessionFactory创建的Session共享该缓存。当通过ID查找实体时(无论是业务代码还是Hibernate内部操作),若该实体启用了二级缓存,会按以下顺序处理:

  1. ✅ 先查一级缓存,命中则直接返回
  2. ⚠️ 一级缓存未命中时,检查二级缓存
    • 若二级缓存存在实体状态数据,则组装实例返回
  3. ❌ 两级缓存均未命中时,从数据库加载数据并组装实例

一旦实体存入持久化上下文(一级缓存),在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.UpdateTimestampsCacheStandardQueryCache已更名为default-update-timestamps-regiondefault-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

这种设计空间效率高,且易于与数据库同步。特殊情况:满足以下条件时,只读实体可存储为直接引用:

  1. 实体类标记@Immutable
  2. 仅包含标量字段(无关联)
  3. 设置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 查询缓存最佳实践

关键注意事项:

  1. ✅ 仅缓存返回实体的ID,强烈建议同时启用这些实体的二级缓存
  2. ❌ 参数组合多的查询(如分页查询)不适合缓存(每个参数组合生成独立缓存条目)
  3. ❌ 涉及高频修改实体的查询不适合缓存(任何相关实体修改都会使缓存失效)
  4. ✅ 默认使用default-query-results-region,可通过自定义区域名实现差异化配置
  5. ⚠️ 特殊区域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仓库


原始标题:Hibernate Second-Level Cache | Baeldung