Hibernate Кэш второго уровня

Hibernate Кэш второго уровня

1. обзор

Одним из преимуществ уровней абстракции базы данных, таких как структуры ORM (объектно-реляционное сопоставление), является ихability to transparently cache data, извлекаемый из базового хранилища. Это помогает устранить затраты на доступ к базе данных для часто используемых данных.

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

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

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

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

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

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

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

  • Если экземпляр уже присутствует в кэше первого уровня, он возвращается оттуда

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

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

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

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

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

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

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


    org.hibernate
    hibernate-ehcache
    5.2.2.Final

Take a look here для последней версииhibernate-ehcache. Однако убедитесь, что версияhibernate-ehcache равна версии Hibernate, которую вы используете в своем проекте,e.g., если вы используете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. Создание кэшируемой сущности

Дляmake an entity eligible for second-level caching мы аннотируем его специальной аннотацией@org.hibernate.annotations.Cache для Hibernate и указываемcache concurrency strategy.

Некоторые разработчики считают, что добавление стандартной аннотации@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 хранятся в кэше с именемcom.example.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.example.hibernate.cache.model.Foo").getSize();
assertThat(size, greaterThan(0));

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

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

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

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

  • READ_ONLY: используется только для сущностей, которые никогда не меняются (исключение выдается, если делается попытка обновить такую ​​сущность). Это очень просто и производительно. Очень подходит для некоторых статических справочных данных, которые не меняются

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

  • READ_WRITE: эта стратегия гарантирует сильную согласованность, которую она достигает с помощью «мягких» блокировок: при обновлении кэшированного объекта в кэше также сохраняется мягкая блокировка для этого объекта, которая снимается после фиксации транзакции. . Все одновременные транзакции, которые получают доступ к программно-заблокированным записям, будут получать соответствующие данные непосредственно из базы данных.

  • TRANSACTIONAL: изменения кеша выполняются в распределенных транзакциях 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, а в разобранном (гидратированном) состоянии:

  • Идентификатор (первичный ключ) не сохраняется (он хранится как часть ключа кэша)

  • Переходные свойства не сохраняются

  • Коллекции не хранятся (подробнее см. Ниже)

  • Значения свойств неассоциированных связей хранятся в их первоначальном виде

  • Для ассоциацийToOne сохраняется только идентификатор (внешний ключ)

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

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

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

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

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

10. Недействительность кеша для запросов в стиле HQL DML и собственных запросов

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

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

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

11. Query Cache

Результаты 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. Рекомендации по использованию кеша запросов

Вот несколькоguidelines and best practices related to query caching:

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

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

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

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

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

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

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

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