Contents

Hibernate 二级缓存简介

1. 概述

诸如 ORM(对象关系映射)框架之类的数据库抽象层的优点之一是它们能够透明地缓存从底层存储中检索到的数据。这有助于消除频繁访问数据的数据库访问成本。

如果缓存内容的读/写比率很高,性能提升可能非常显着,尤其是对于由大型对象图组成的实体。

在本文中,我们将探索 Hibernate 二级缓存。

我们解释了一些基本概念,并且一如既往地用简单的例子来说明一切。我们使用 JPA 并仅针对 JPA 中未标准化的那些特性回退到 Hibernate 原生 API。

2. 什么是二级缓存?

与大多数其他配备齐全的 ORM 框架一样,Hibernate 具有一级缓存的概念。它是一个会话范围的缓存,可确保每个实体实例在持久上下文中只加载一次。

一旦会话关闭,一级缓存也会终止。这实际上是可取的,因为它允许并发会话与彼此隔离的实体实例一起工作。

另一方面,二级缓存是SessionFactory范围的,这意味着它由使用同一会话工厂创建的所有会话共享。当实体实例通过其 id 查找时(通过应用程序逻辑或 Hibernate 内部,例如,当它从其他实体加载到该实体的关联时),并且如果为该实体启用了二级缓存,则会发生以下情况:

  • 如果一个实例已经存在于一级缓存中,则从那里返回
  • 如果在一级缓存中没有找到实例,而在二级缓存中缓存了对应的实例状态,则从那里取出数据并组装一个实例并返回
  • 否则,从数据库中加载必要的数据并组装并返回一个实例

一旦实例存储在持久性上下文(一级缓存)中,它就会在同一会话内的所有后续调用中从那里返回,直到会话关闭或实例被手动从持久性上下文中逐出。此外,加载的实例状态存储在 L2 缓存中(如果尚未存在)。

3. 区域工厂

Hibernate 二级缓存被设计为不知道实际使用的缓存提供程序。Hibernate 只需要提供org.hibernate.cache.spi.RegionFactory接口的实现,该接口封装了特定于实际缓存提供者的所有细节。基本上,它充当 Hibernate 和缓存提供者之间的桥梁。

在本文中**,我们使用 Ehcache 作为缓存提供者**,它是一种成熟且应用广泛的缓存。当然,您可以选择任何其他提供者,只要有RegionFactory的实现即可。

我们使用以下 Maven 依赖项将 Ehcache 区域工厂实现添加到类路径:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>5.2.2.Final</version>
</dependency>

在这里 查看最新版本的hibernate-ehcache。但是,请确保hibernate-ehcache版本等于您在项目中使用的 Hibernate 版本,例如,如果您使用hibernate-ehcache 5.2.2.Final就像在这个例子中一样,那么 Hibernate 的版本也应该是5.2.2.Final

hibernate-ehcache工件依赖于 Ehcache 实现本身,因此它也可传递地包含在类路径中。

4. 启用二级缓存

通过以下两个属性,我们告诉 Hibernate L2 缓存已启用,我们将其命名为区域工厂类的名称:

hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory

例如,在persistence.xml中它看起来像:

<properties>
    ...
    <property name="hibernate.cache.use_second_level_cache" value="true"/>
    <property name="hibernate.cache.region.factory_class" 
      value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
    ...
</properties>

要禁用二级缓存(例如出于调试目的),只需将hibernate.cache.use_second_level_cache属性设置为 false。

5. 使实体可缓存

为了使实体有资格进行二级缓存,我们使用 Hibernate 特定的*@org.hibernate.annotations.Cache*对其进行注解,并指定缓存并发策略

一些开发人员认为添加标准的*@javax.persistence.Cacheable*注解也是一个很好的约定(尽管 Hibernate 不需要),因此实体类实现可能如下所示:

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Foo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private long id;
    @Column(name = "NAME")
    private String name;
    
    // getters and setters
}

对于每个实体类,Hibernate 将使用一个单独的缓存区域来存储该类的实例状态。区域名称是完全限定的类名称。 例如,Foo实例存储在 Ehcache 中名为com.blogdemo.hibernate.cache.model.Foo的缓存中。

为了验证缓存是否正常工作,我们可以编写一个这样的快速测试:

Foo foo = new Foo();
fooService.create(foo);
fooService.findOne(foo.getId());
int size = CacheManager.ALL_CACHE_MANAGERS.get(0)
  .getCache("com.blogdemo.hibernate.cache.model.Foo").getSize();
assertThat(size, greaterThan(0));

这里我们直接使用 Ehcache API 来验证我们加载一个Foo实例后com.blogdemo.hibernate.cache.model.Foo缓存不为空。

您还可以启用 Hibernate 生成的 SQL 的日志记录,并在测试中多次调用fooService.findOne(foo.getId())以验证用于加载Fooselect语句仅打印一次(第一次),这意味着在后续调用实体实例是从缓存中获取的。

6. 缓存并发策略

根据用例,我们可以自由选择以下缓存并发策略之一:

  • READ_ONLY:仅用于永不更改的实体(如果尝试更新此类实体,则会引发异常)。它非常简单且高效。非常适合一些不变的静态参考数据
  • NONSTRICT_READ_WRITE:在提交更改受影响数据的事务后更新缓存。因此,不能保证强一致性,并且有一个小的时间窗口可以从缓存中获取陈旧的数据。这种策略适用于可以容忍最终一致性的用例
  • READ_WRITE:此策略通过使用“软”锁来保证强一致性:当更新缓存的实体时,该实体的缓存中也会存储一个软锁,该软锁在事务提交后释放。所有访问软锁定条目的并发事务将直接从数据库中获取相应的数据
  • TRANSACTIONAL:缓存更改在分布式 XA 事务中完成。缓存实体中的更改在同一个 XA 事务中的数据库和缓存中提交或回滚

7. 缓存管理

如果未定义过期和逐出策略,缓存可能会无限增长并最终消耗所有可用内存。在大多数情况下,Hibernate 将此类缓存管理职责留给缓存提供者,因为它们确实特定于每个缓存实现。

例如,我们可以定义以下 Ehcache 配置,将缓存的Foo实例的最大数量限制为 1000:

<ehcache>
    <cache name="com.blogdemo.persistence.model.Foo" maxElementsInMemory="1000" />
</ehcache>

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(外键)

这描述了一般的 Hibernate 二级缓存设计,其中缓存模型反映了底层的关系模型,它节省空间并且很容易保持两者同步。

9.1. 缓存集合的内部表示

我们已经提到,我们必须明确指出一个集合(OneToManyManyToMany关联)是可缓存的,否则它不会被缓存。 实际上,Hibernate 将集合存储在单独的缓存区域中,每个集合一个。区域名称是一个完全限定的类名称加上集合属性的名称,例如:com.blogdemo.hibernate.cache.model.Foo.bars。这使我们可以灵活地为集合定义单独的缓存参数,例如删除/过期策略。

此外,重要的是要提到每个集合条目只缓存集合中包含的实体的 id,这意味着在大多数情况下,最好将包含的实体也设为可缓存。

10. HQL DML 样式查询和本机查询的缓存失效

对于 DML 样式的 HQL(insertupdatedeleteHQL 语句),Hibernate 能够确定哪些实体受到此类操作的影响:

entityManager.createQuery("update Foo set … where …").executeUpdate();

在这种情况下,所有 Foo 实例都从 L2 缓存中逐出,而其他缓存的内容保持不变。

但是,当涉及到原生 SQL DML 语句时,Hibernate 无法猜测正在更新什么,因此它会使整个二级缓存失效:

session.createNativeQuery("update FOO set … where …").executeUpdate();

这可能不是你想要的!解决方案是告诉 Hibernate 哪些实体受到原生 DML 语句的影响,这样它就可以只驱逐与Foo实体相关的条目:

Query nativeQuery = entityManager.createNativeQuery("update FOO set ... where ...");
nativeQuery.unwrap(org.hibernate.SQLQuery.class).addSynchronizedEntityClass(Foo.class);
nativeQuery.executeUpdate();

我们也回退到 Hibernate 原生SQLQuery API,因为这个特性(还)没有在 JPA 中定义。

请注意,上述内容仅适用于 DML 语句(insertupdatedelete和本机函数/过程调用)。本机select查询不会使缓存无效。

11. 查询缓存

HQL 查询的结果也可以被缓存。如果您经常对很少更改的实体执行查询,这很有用。

要启用查询缓存,请将hibernate.cache.use_query_cache属性的值设置为true

hibernate.cache.use_query_cache=true

然后,对于每个查询,您必须明确指出该查询是可缓存的(通过org.hibernate.cacheable查询提示):

entityManager.createQuery("select f from Foo f")
  .setHint("org.hibernate.cacheable", true)
  .getResultList();

11.1.查询缓存最佳实践

以下是与查询缓存相关的一些指南和最佳实践

  • 与集合的情况一样,仅缓存作为可缓存查询返回的实体的 ID,因此强烈建议为此类实体启用二级缓存。
  • 每个查询的每个查询参数值(绑定变量)组合都有一个缓存条目,因此您期望有许多不同参数值组合的查询不适合缓存。
  • 涉及数据库中经常更改的实体类的查询也不适合缓存,因为只要与参与查询的任何实体类相关的更改发生更改,它们就会失效,无论更改的实例是否是否缓存为查询结果的一部分。
  • 默认情况下,所有查询缓存结果都存储在org.hibernate.cache.internal.StandardQueryCache区域中。与实体/集合缓存一样,您可以为此区域自定义缓存参数,以根据您的需要定义驱逐和过期策略。对于每个查询,您还可以指定自定义区域名称,以便为不同的查询提供不同的设置。
  • 对于作为可缓存查询的一部分查询的所有表,Hibernate 将上次更新时间戳保存在名为org.hibernate.cache.spi.UpdateTimestampsCache的单独区域中。如果您使用查询缓存,则了解该区域非常重要,因为 Hibernate 使用它来验证缓存的查询结果是否过时。只要查询结果区域中的相应表有缓存的查询结果,此缓存中的条目就不能被驱逐/过期。最好关闭此缓存区域的自动驱逐和过期,因为它无论如何都不会消耗大量内存。