Кеш второго уровня
Контекст персистентности — кеш первого уровня, но живёт он, пока жив один 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 есть отдельный кеш запросов, но он редко окупается: он сбрасывается при каждом изменении таблицы.