Рубрики
Без рубрики

Спящий режим Кэш второго Уровня

Руководство по кэшу второго уровня Hibernate и как его использовать на практике.

Автор оригинала: baeldung.

1. Обзор

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

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

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

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

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

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

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

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

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

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

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

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

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

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


    org.hibernate
    hibernate-ehcache
    5.2.2.Final

Посмотрите здесь для последней версии 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 это будет выглядеть так:


    ...
    
    
    ...

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

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

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

Некоторые разработчики считают, что было бы неплохо добавить стандарт @javax.persistence.Также кэшируемая аннотация (хотя и не требуется для 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 хранятся в кэше с именем com.baeldung.hibernate.cache.model.Foo в Ehcache.

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

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

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

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

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

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

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

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

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

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


    

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

    // getters and setters
}

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

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

  • Идентификатор (первичный ключ) не сохраняется (он хранится как часть ключа кэша)
  • Переходные свойства не сохраняются
  • Коллекции не хранятся (подробнее см. Ниже)
  • Значения свойств, не связанных с ассоциацией, хранятся в их первоначальном виде
  • Только идентификатор (внешний ключ) хранится для До одной ассоциации

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

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

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

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

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

10. Аннулирование кэша для запросов в стиле HQL DML и собственных запросов

Когда речь заходит о HQL в стиле DML ( insert , update и delete HQL-операторах), 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();

Мы должны вернуться в спящий режим SQL-запрос API, поскольку эта функция (пока) не определена в JPA.

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

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

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

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

hibernate.cache.use_query_cache=true

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

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

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

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

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

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

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

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