Перейти к основному содержимому

Спящий режим кэша второго уровня

· 10 мин. чтения

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:

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

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

Артефакт 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 это будет выглядеть так:

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

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

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

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

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

Здесь мы используем Ehcache API напрямую, чтобы убедиться, что кеш com.foreach.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:

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

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

// getters and setters
}

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

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

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

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

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

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

На самом деле Hibernate хранит коллекции в отдельных областях кеша, по одной на каждую коллекцию. Имя региона представляет собой полное имя класса плюс имя свойства коллекции, например: com.foreach.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();

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

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

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

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

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

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

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

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

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