«1. Обзор

Одним из преимуществ уровней абстракции базы данных, таких как фреймворки ORM (объектно-реляционное отображение), является их способность прозрачно кэшировать данные, извлеченные из нижележащего хранилища. Это помогает устранить затраты на доступ к базе данных для часто используемых данных.

Прирост производительности может быть значительным, если соотношение чтения/записи кэшированного содержимого велико, особенно для сущностей, состоящих из больших графов объектов.

В этой статье мы исследуем кэш второго уровня Hibernate.

Мы объясняем некоторые основные понятия и, как всегда, иллюстрируем все простыми примерами. Мы используем JPA и возвращаемся к собственному API Hibernate только для тех функций, которые не стандартизированы в JPA.

2. Что такое кэш второго уровня?

Как и в большинстве других полнофункциональных фреймворков ORM, в Hibernate используется концепция кеша первого уровня. Это кеш на уровне сеанса, который гарантирует, что каждый экземпляр сущности загружается только один раз в постоянном контексте.

После закрытия сеанса кеш первого уровня также завершается. На самом деле это желательно, поскольку позволяет параллельным сеансам работать с экземплярами сущностей изолированно друг от друга.

С другой стороны, кеш второго уровня имеет область действия SessionFactory, то есть он совместно используется всеми сеансами, созданными с помощью одной и той же фабрики сеансов. Когда экземпляр объекта ищется по его идентификатору (либо с помощью логики приложения, либо внутри Hibernate, например, когда он загружает ассоциации с этим объектом из других объектов), и если для этого объекта включено кэширование второго уровня, происходит следующее: ~ ~~ Если экземпляр уже присутствует в кеше первого уровня, он возвращается оттуда Если экземпляр не найден в кеше первого уровня, а соответствующее состояние экземпляра закэшировано в кеше второго уровня, то данные извлекается оттуда, экземпляр собирается и возвращается В противном случае необходимые данные загружаются из базы данных, экземпляр собирается и возвращается

    Как только экземпляр сохраняется в контексте персистентности (кэш первого уровня), он возвращается оттуда во всех последующих вызовах в рамках того же сеанса, пока сеанс не будет закрыт или экземпляр не будет вручную исключен из контекста постоянства. Кроме того, состояние загруженного экземпляра сохраняется в кеше L2, если его там еще нет.

3. Фабрика регионов

Кэширование второго уровня Hibernate спроектировано таким образом, чтобы не знать о фактическом используемом поставщике кэша. Hibernate должен быть обеспечен только реализацией интерфейса org.hibernate.cache.spi.RegionFactory, который инкапсулирует все детали, относящиеся к фактическим поставщикам кэша. По сути, он действует как мост между Hibernate и провайдерами кеша.

В этой статье мы используем Ehcache в качестве поставщика кэша, который является зрелым и широко используемым кэшем. Конечно, вы можете выбрать любого другого поставщика, если для него существует реализация RegionFactory.

Мы добавляем реализацию фабрики региона Ehcache в путь к классам со следующей зависимостью Maven:

Посмотрите здесь последнюю версию hibernate-ehcache. Однако убедитесь, что версия hibernate-ehcache равна версии Hibernate, которую вы используете в своем проекте, например если вы используете hibernate-ehcache 5.2.2.Final, как в этом примере, то версия Hibernate также должна быть 5.2.2.Final.

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

Артефакт hibernate-ehcache зависит от самой реализации Ehcache, которая, таким образом, также транзитивно включена в путь к классам.

4. Включение кэширования второго уровня

С помощью следующих двух свойств мы сообщаем Hibernate, что кэширование L2 включено, и даем ему имя фабричного класса региона:

Например, in persistence. xml это будет выглядеть так:

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

Чтобы отключить кэширование второго уровня (например, в целях отладки), просто установите для свойства hibernate.cache.use_second_level_cache значение false.

<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>

5. Делаем объект кэшируемым

«Чтобы сделать сущность подходящей для кэширования второго уровня, мы аннотируем ее специфичной для Hibernate аннотацией @org.hibernate.annotations.Cache и указываем стратегию параллелизма кэширования.

Некоторые разработчики считают хорошим соглашением добавить стандартную аннотацию @javax.persistence.Cacheable (хотя это и не требуется для Hibernate), поэтому реализация класса сущностей может выглядеть так:

Для каждого класса сущностей, 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
}

Например, экземпляры Foo хранятся в кэше с именем com.baeldung.hibernate.cache.model.Foo в Ehcache.

Чтобы убедиться, что кеширование работает, мы можем написать такой быстрый тест:

Здесь мы используем Ehcache API напрямую, чтобы убедиться, что кеш com.baeldung.hibernate.cache.model.Foo не пуст после мы загружаем экземпляр Foo.

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

Вы также можете включить ведение журнала SQL, сгенерированного Hibernate, и вызвать fooService.findOne(foo.getId()) несколько раз в тесте, чтобы убедиться, что оператор select для загрузки Foo печатается только один раз (в первый раз), что означает что при последующих вызовах экземпляр объекта извлекается из кеша.

6. Стратегия параллелизма кэша

В зависимости от вариантов использования мы можем выбрать одну из следующих стратегий параллелизма кэша:

READ_ONLY: Используется только для сущностей, которые никогда не такой объект создан). Это очень просто и эффективно. Очень подходит для некоторых статических эталонных данных, которые не изменяются. NONSTRICT_READ_WRITE: кэш обновляется после фиксации транзакции, которая изменила затронутые данные. Таким образом, строгая согласованность не гарантируется, и существует небольшое временное окно, в течение которого устаревшие данные могут быть получены из кэша. Стратегия такого типа подходит для вариантов использования, допускающих возможную непротиворечивость. READ_WRITE: эта стратегия гарантирует строгую согласованность, которая достигается за счет использования «мягких» блокировок: при обновлении кэшированного объекта в кэше сохраняется программная блокировка для этого объекта. сущность, которая освобождается после фиксации транзакции. Все параллельные транзакции, которые обращаются к мягко заблокированным записям, будут извлекать соответствующие данные непосредственно из базы данных. ТРАНЗАКЦИОННЫЙ: изменения кэша выполняются в распределенных транзакциях XA. Изменение в кэшированном объекте либо фиксируется, либо откатывается как в базе данных, так и в кэше в одной и той же транзакции XA

    7. Управление кэшем

Если политики истечения срока действия и вытеснения не определены, кэш может расти бесконечно и в конечном итоге поглотить все доступной памяти. В большинстве случаев Hibernate оставляет такие обязанности по управлению кешем поставщикам кеша, поскольку они действительно специфичны для каждой реализации кеша.

Например, мы могли бы определить следующую конфигурацию Ehcache, чтобы ограничить максимальное количество кэшированных экземпляров Foo до 1000:

8. Кэш коллекций

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

Коллекции не кэшируются по умолчанию, и нам нужно явно пометить их как кэшируемые. Например:

9. Внутреннее представление кэшированного состояния

@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
}

Сущности не хранятся в кэше второго уровня как экземпляры Java, а скорее находятся в дизассемблированном (гидратированном) состоянии:

Id (первичный ключ) не сохраняется (хранится как часть ключа кеша) Временные свойства не сохраняются Коллекции не сохраняются (подробнее см. ниже) Значения неассоциативных свойств сохраняются в исходной форме Связи ToOne

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

9.1. Внутреннее представление кэшированных коллекций

Мы уже упоминали, что должны явно указать, что коллекция (ассоциация OneToMany или ManyToMany) кэшируется, иначе она не кэшируется.

«На самом деле Hibernate хранит коллекции в отдельных областях кеша, по одной на каждую коллекцию. Имя региона представляет собой полное имя класса плюс имя свойства коллекции, например: com.baeldung.hibernate.cache.model.Foo.bars. Это дает нам возможность определять отдельные параметры кеша для коллекций, например. политика выселения/истечения срока действия.

Кроме того, важно отметить, что для каждой записи коллекции кешируются только идентификаторы сущностей, содержащихся в коллекции, а это означает, что в большинстве случаев хорошей идеей является также кэширование содержащихся сущностей.

10. Инвалидация кеша для HQL-запросов в стиле DML и нативных запросов

Когда дело доходит до HQL-стиля DML (инструкции вставки, обновления и удаления HQL), Hibernate может определить, на какие объекты влияют такие операции:

В этом случае все экземпляры Foo удаляются из кеша L2, в то время как остальное кэшированное содержимое остается неизменным.

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

Однако, когда дело доходит до нативных операторов SQL DML, Hibernate не может угадать, что именно обновляется, поэтому делает недействительным весь кеш второго уровня:

Вероятно, это не то, что вам нужно! Решение состоит в том, чтобы сообщить Hibernate, на какие сущности влияют собственные операторы DML, чтобы он мог исключать только записи, связанные с сущностями Foo: еще) определено в JPA.

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

Обратите внимание, что приведенное выше относится только к операторам DML (вставка, обновление, удаление и собственные вызовы функций/процедур). Собственные запросы на выборку не делают кеш недействительным.

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

11. Кэш запросов

Результаты запросов HQL также можно кэшировать. Это полезно, если вы часто выполняете запрос к объектам, которые редко изменяются.

Чтобы включить кеширование запросов, установите для свойства hibernate.cache.use_query_cache значение true:

Затем для каждого запроса вы должны явно указать, что запрос кэшируется (через org.hibernate. подсказка запроса):

11.1. Рекомендации по кэшированию запросов

hibernate.cache.use_query_cache=true

Вот некоторые рекомендации и рекомендации, связанные с кэшированием запросов:

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

Как и в случае с коллекциями, кэшируются только идентификаторы сущностей, возвращенные в результате кэшируемого запроса, поэтому настоятельно рекомендуется для таких сущностей включен кеш второго уровня. Существует одна запись кэша для каждой комбинации значений параметров запроса (переменных привязки) для каждого запроса, поэтому запросы, для которых вы ожидаете множество различных комбинаций значений параметров, не являются хорошими кандидатами для кэширования. Запросы, включающие классы сущностей, для которых в базе данных происходят частые изменения, также не являются хорошими кандидатами для кэширования, потому что они будут признаны недействительными всякий раз, когда происходит изменение, относящееся к любому из классов сущностей, участвующих в запросе, независимо от того, являются ли измененные экземпляры кэшируется как часть результата запроса или нет. По умолчанию все результаты кэширования запросов хранятся в регионе org.hibernate.cache.internal.StandardQueryCache. Как и в случае кэширования сущностей/коллекций, вы можете настроить параметры кэша для этого региона, чтобы определить политики исключения и истечения срока действия в соответствии с вашими потребностями. Для каждого запроса вы также можете указать собственное имя региона, чтобы предоставить разные настройки для разных запросов. Для всех таблиц, которые запрашиваются как часть кешируемых запросов, Hibernate хранит метки времени последнего обновления в отдельной области с именем org.hibernate.cache.spi.UpdateTimestampsCache. Знание этой области очень важно, если вы используете кэширование запросов, потому что Hibernate использует его для проверки того, что результаты кэшированных запросов не устарели. Записи в этом кэше не должны быть вытеснены или просрочены, пока есть кэшированные результаты запросов для соответствующих таблиц в областях результатов запросов. Лучше всего отключить автоматическое вытеснение и истечение срока действия для этой области кэша, так как она в любом случае не потребляет много памяти.

12. Заключение

    «В этой статье мы рассмотрели, как настроить кэш второго уровня Hibernate. Мы увидели, что его довольно легко настроить и использовать, поскольку Hibernate выполняет всю тяжелую работу за кулисами, делая использование кэша второго уровня прозрачным для бизнес-логики приложения.

Реализация этого руководства по кэшированию второго уровня Hibernate доступна на Github. Это проект на основе Maven, поэтому его легко импортировать и запускать как есть.

«

The implementation of this Hibernate Second-Level Cache Tutorial is available on Github. This is a Maven based project, so it should be easy to import and run as it is.