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?
Все вопросы по теме: список
Все темы: список
Вопросы/замечания/предложения/нашли ошибку: напишите мне