34. Как работать с кэшем 2 уровня?

34. Как работать с кэшем 2 уровня?

UNKNOWN

Если  кэш  первого  уровня  привязан  к  объекту  сессии,  то  кэш  второго  уровня привязан к объекту-фабрике сессий (Session Factory object). Что как бы подразумевает, что видимость этого кэша гораздо шире кэша первого уровня. 

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

Hibernate поставляется со встроенной поддержкой стандарта кэширования Java JCache, а также двух популярных библиотек кэширования: Ehcache и Infinispan.

Shared Cache Mode

Будут  ли  в  нашем  приложении  кэшироваться  сущности  и  связанные  с  ними состояния,  определяется  значением  элемента  shared-cache-mode  файла persistence.xml (или в свойстве javax.persistence.sharedCache.mode конфигурационного файла). Если в файле для элемента shared-cache-mode установлено значение: 

  • ENABLE_SELECTIVE (дефолтное и рекомендуемое значение): только сущности с  аннотацией  @Cacheable  (равносильно  значению  по  умолчанию @Cacheable(value=true)) будут сохраняться в кэше второго уровня.
  • DISABLE_SELECTIVE: все сущности будут сохраняться в кэше второго уровня, за  исключением  сущностей,  помеченных  аннотацией  @Cacheable(value=false) как некэшируемые.
  • ALL: сущности всегда кэшируются, даже если они помечены как некэшируемые.
  • NONE: ни одна сущность не кэшируется, даже если помечена как кэшируемая. При данной опции имеет смысл вообще отключить кэш второго уровня.
  • UNSPECIFIED: применяются значения по умолчанию для кэша второго уровня, определенные Hibernate. Это эквивалентно тому, что вообще не используется shared-cache-mode,  так  как  Hibernate  не  включает  кэш  второго  уровня,  если используется режим UNSPECIFIED.

В Hibernate кэширование второго уровня реализовано в виде абстракции, то есть мы должны предоставить любую её реализацию, вот несколько провайдеров: Ehcache, OSCache, SwarmCache, JBoss TreeCache. Для Hibernate требуется только реализация интерфейса org.hibernate.cache.spi.RegionFactory, который инкапсулирует все детали, относящиеся  к  конкретным провайдерам.  По сути,  RegionFactory действует  как  мост между Hibernate и поставщиками кэша. В примерах будем использовать Ehcache. Что нужно сделать:

  • добавить мавен-зависимость кэш-провайдера нужной версии:

<dependency>

     <groupId>org.hibernate</groupId>

     <artifactId>hibernate-ehcache</artifactId>

     <version>5.2.2.Final</version>

</dependency>

  • включить кэш второго уровня и определить конкретного провайдера:

hibernate.cache.use_second_level_cache=true

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

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

@Entity

@Table(name = \"shared_doc\")

@Cacheable

@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)

public class SharedDoc{

    private Set<User> users;

}

  • не обязательно устанавливать у сущностей JPA-аннотацию @Cacheable, если работаем с Hibernate напрямую, не через JPA. 
  • чтобы  кэш  не  “съел”  всю  доступную  память,  можно,  например,  ограничивать количество каждого типа сущностей, хранимых в кэше:

<ehcache>

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

</ehcache>

Стратегия параллельного доступа к объектам

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

  • READ_ONLY:  Используется  только  для  сущностей,  которые  никогда  не изменяются (будет выброшено исключение, если попытаться обновить такую сущность).  Очень  просто  и  производительно.  Подходит  для  некоторых статических данных, которые не меняются.
  • NONSTRICT_READ_WRITE:  Кэш  обновляется  после  совершения  транзакции, которая  изменила  данные  в  БД  и  закоммитила  их.  Таким  образом,  строгая согласованность  не  гарантируется,  и  существует  небольшое  временное  окно между обновлением данных в БД и обновлением тех же данных в кэше, во время которого параллельная транзакция может получить из кэша устаревшие данные.
  • READ_WRITE: Эта стратегия гарантирует строгую согласованность, которую она достигает,  используя  «мягкие»  блокировки:  когда  обновляется  кэшированная сущность, на нее накладывается мягкая блокировка, которая снимается после коммита транзакции. Все параллельные транзакции, которые пытаются получить доступ  к  записям  в  кэше  с  наложенной  мягкой  блокировкой,  не  смогут  их прочитать  или  записать  и  отправят  запрос  в  БД.  Ehcache  использует  эту стратегию по умолчанию.
  • TRANSACTIONAL:  полноценное  разделение  транзакций.  Каждая  сессия  и каждая  транзакция  видят  объекты,  как  если  бы  только  они  с  ним  работали последовательно  одна  транзакция  за  другой.  Плата  за  это  —  блокировки  и потеря производительности.

Представление объектов в кэше

Еще одна важная деталь про кэш второго уровня о которой стоило бы упомянуть — Hibernate не хранит сами объекты Ваших классов. Он хранит информацию в виде массивов  строк,  чисел  и  т.д.  Что  очень  разумно,  учитывая  сколько  лишней  памяти занимает  каждый  объект.  Идентификатор  объекта  выступает  указателем  на  эту информацию. Концептуально  это  нечто  вроде  Map, в  которой  id  объекта  —  ключ,  а массивы данных — значения полей. Приблизительно это можно представить себе так:

1 -> { \"Pupkin\", 1, null , {1,2,5} }

Помимо  вышесказанного,  следует  помнить  —  зависимости  Вашего  класса  по умолчанию  также  не  кэшируются.  Например,  рассмотрим  класс  SharedDoc,  который кэшируется:

@Entity

@Table(name = \"shared_doc\")

@Cacheable

@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)

public class SharedDoc{

    private Set<User> users;

}

В  примере  выше  при  выборке  сущности  SharedDoc  из  кэша,  коллекция  users будет доставаться из БД, а не из кэша второго уровня. Если мы хотим также кэшировать и  зависимости,  то  над  полями  тоже  нужно  разместить  аннотации  @Cacheable  и @Cache:

@Entity

@Table(name = \"shared_doc\")

@Cacheable

@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)

public class SharedDoc{

    @Cacheable

    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)

    private Set<User> users;

}

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

@Cache

Это аннотация Hibernate, настраивающая тонкости кэширования объекта в кэше второго уровня Hibernate. @Cache принимает три параметра:

  • include  -  имеет  по  умолчанию  значение  all  и  означающий  кэширование  всего объекта. Второе возможное значение - non-lazy, запрещает кэширование лениво загружаемых  объектов.  Кэш  первого  уровня  не  обращает  внимания  на  эту директиву и всегда кэширует лениво загружаемые объекты.
  • region  -  позволяет  задать  имя  региона  кэша  для  хранения  сущности.  Регион можно представить как разные области кэша, имеющие разные настройки на уровне реализации кэша. Например, можно было бы создать в конфигурации ehcache  два  региона,  один  с  краткосрочным  хранением  объектов,  другой  с долгосрочным и отправлять часто изменяющиеся объекты в первый регион, а все остальные - во второй. Ehcache по умолчанию создает регион для каждой сущности  с  именем  класса  этой  сущности,  соответственно  в  этом  регионе хранятся только эти сущности. К примеру, экземпляры Foo хранятся в Ehcache в кэше с именем “com.baeldung.hibernate.cache.model.Foo”.
  • usage - задаёт стратегию одновременного доступа к объектам.

Кэш запросов (Query Cache)

Результаты HQL-запросов также могут быть кэшированы. Это полезно, если мы часто выполняем запрос к объектам, которые редко меняются. Чтобы включить кэш запросов, установите для свойства hibernate.cache.use_query_cache значение true:

hibernate.cache.use_query_cache=true

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

entityManager.createQuery(\"select f from Foo f\") 

  .setHint(\"org.hibernate.cacheable\", true)

  .getResultList();

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

У  кэша  запросов  есть и  своя  цена  —  Hibernate  будет  вынужден  отслеживать сущности закешированные с определённым запросом и выкидывать запрос из кэша, если  кто-то  поменяет  значение  сущности.  То  есть  для  кэша  запросов  стратегия параллельного доступа всегда read-only.


Предыдущий вопрос: 33. Какие два вида кэшей (cache) вы знаете в JPA и для чего они нужны?

Следующий вопрос: 35. Что такое JPQL HQL и чем он отличается от SQL?

Все вопросы по теме: список

Все темы: список

Вопросы/замечания/предложения/нашли ошибку: напишите мне

Report Page