Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Кеш второго уровня

Контекст персистентности — кеш первого уровня, но живёт он, пока жив один EntityManager, обычно один запрос. Кеш второго уровня общий для всей EntityManagerFactory: сущность, загруженную в одном запросе, в следующем можно взять из памяти без обращения к базе. Он подходит для данных, которые часто читают и редко меняют, например тегов или категорий.

Зависимости

Hibernate хранит кеш через стандарт JCache, а само хранилище даёт библиотека, здесь Caffeine:

<!-- pom.xml -->
<dependency>
  <groupId>org.hibernate.orm</groupId>
  <artifactId>hibernate-jcache</artifactId>
  <version>7.4.12.Final</version>
</dependency>
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>jcache</artifactId>
  <version>3.3.0</version>
</dependency>

Настройки

<!-- src/main/resources/META-INF/persistence.xml -->
<persistence-unit name="blog">
  <class>com.example.blog.Author</class>
  <class>com.example.blog.Post</class>
  <class>com.example.blog.Comment</class>
  <class>com.example.blog.Tag</class>
  <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
  <properties>
    <!-- свойства подключения, как раньше -->
    <property name="hibernate.cache.region.factory_class" value="jcache"/>
    <property name="hibernate.javax.cache.missing_cache_strategy" value="create"/>
    <property name="hibernate.generate_statistics" value="true"/>
  </properties>
</persistence-unit>

ENABLE_SELECTIVE означает «кешировать только отмеченные сущности». missing_cache_strategy разрешает Hibernate самому создавать области кеша, без отдельного файла настроек Caffeine.

Пометка сущности

// src/main/java/com/example/blog/Tag.java
@Entity
@Cacheable
public class Tag {
    // поля, как раньше
}

@Cacheable импортируется из jakarta.persistence.

Проверка

// src/main/java/com/example/blog/Main.java
emf.runInTransaction(em -> em.persist(new Tag("java")));
 
for (int i = 1; i <= 3; i++) {
    Tag tag = emf.callInTransaction(em -> em.find(Tag.class, 1L));
    IO.println("Запрос " + i + ": " + tag.getName());
}
 
Statistics stats = emf.unwrap(SessionFactory.class).getStatistics();
IO.println("Попаданий в кеш: " + stats.getSecondLevelCacheHitCount()
        + ", промахов: " + stats.getSecondLevelCacheMissCount());

Каждый callInTransaction использует новый EntityManager, так что кеш первого уровня здесь не помогает. Вывод:

INFO: HHH90001028: Second-level cache region factory [org.hibernate.cache.jcache.internal.JCacheRegionFactory]
Hibernate: insert into Tag (name,id) values (?,?)
Запрос 1: java
Запрос 2: java
Запрос 3: java
Попаданий в кеш: 3, промахов: 0

Ни одного select: тег попал в кеш при сохранении, и все три запроса взяли его оттуда. Без @Cacheable та же программа трижды выполняет select t1_0.id,t1_0.name from Tag t1_0 where t1_0.id=? и сообщает Попаданий в кеш: 0, промахов: 0.

Что нужно помнить

  • Кеш знает только об изменениях, сделанных через Hibernate. Если таблицу меняет другое приложение или нативный SQL update, кеш вернёт устаревшие данные.
  • В кластере из нескольких серверов у каждого свой кеш Caffeine; для общего кеша нужен распределённый провайдер.
  • Начинайте без кеша, измеряйте и добавляйте его только тем сущностям, где он действительно экономит запросы.

Частая ошибка

Кеш сущностей работает для find и для загрузки связей, но не для запросов. JPQL-запрос по имени всё равно каждый раз идёт в базу:

// src/main/java/com/example/blog/Main.java
Tag tag = emf.callInTransaction(em -> em
        .createQuery("select t from Tag t where t.name = :name", Tag.class)
        .setParameter("name", "java")
        .getSingleResult());
Hibernate: select t1_0.id,t1_0.name from Tag t1_0 where t1_0.name=?
Запрос 1: java
Hibernate: select t1_0.id,t1_0.name from Tag t1_0 where t1_0.name=?
Запрос 2: java

Для этого у Hibernate есть отдельный кеш запросов, но он редко окупается: он сбрасывается при каждом изменении таблицы.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.