Как работает кэш второго уровня Hibernate? Примеры

Введение

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

  • Кэш первого уровня в Hibernate включен по умолчанию и работает на уровне сессии. Прочтите здесь подробнее о нем.
  • Кэш второго уровня не включен по умолчанию и доступен глобально на уровне Session Factory.

Один раз, попав в кэш второго уровня объект-сущность, используется на всем протяжении жизни объекта sessionFactory, т.е. область действия кэша  — вся наша программа, а если быть точнее, то, как вы настроите свой кэш, так он себя и поведет. Это также означает, что если фабрика сессий закроется, весь кэш ассоциированный с ней «умрет» и кэш менеджер тоже выключится.

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

Так же это означает что если у Вас есть два инстанса фабрики сессий (нормальное приложение конечно не будет так разрабатываться), Вы будете иметь два кэш менеджера в Вашем приложении и можно получить непредсказуемые результаты.

flush1

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

Но перед этим разъясню понятие «инвалидация«.  В документации часто встречается понятие инвалидация (invalidated) кэша,  это устаревание данных в кэше,  после того как кэш перешел в это состояние он должен быть очищен от старых данных методом удаления и заново обновлен новыми значениями, после обновления, кэш переходит в  валидное(validate) состояние.

Как работает кэш второго уровня

Давайте напишем все факты:

  1. Кэш первого уровня в первую очередь кэширует сущности, но поддерживает опционально кэш запросов.
  2. Всякий раз когда сессия Hibernate пытается загрузить сущность, самым первом местом где она будет ее искать это кэшированная копия сущности из кэша первого уровня (ассоциированный с частичной сессией).
  3. Если кэшированная копия сущности присутствует в кэше первого уровня, она вернется как результат метода load().
  4. Если не найдена в кэше первого уровня, то ищется в кэше второго уровня (если он включен).
  5. Если в кэше второго уровня имеется кэшированная сущность, она вернется как результат метода load(). Но, прежде чем вернуть сущность, она сохранится в кэше первого уровня так же, поэтому следующий вызов метода загрузки сущности вернет сущность из кэша первого уровня и больше не понадобится искать ее в кэше второго уровня опять.
  6. Если же сущность не была найдена ни в кэше первого уровня, ни в кэше второго уровня, то выполнится запрос в БД и сущность сохранится в обоих кэшах, прежде чем вернется как результат вызова метода load().
  7. Кэш второго уровня  сам следит за обновлениями сущностей из кэша, если модификации были выполнены через Session API.
  8. Если некоторый пользователь или процесс делает изменения прямо в БД, то кэш второго уровня нет имеет возможности обновиться до времени «timeToLiveSeconds» действующего для данного региона кэша. В это случае, хорошей идеей является инвалидировать весь кэш и позволить Hibernate построить его кэш заново. Вы можете использовать следующий код для инвалидации всего кэша второго уровня:

/**
* Evicts all second level cache hibernate entites. This is generally only
* needed when an external application modifies the database.
*/
public void evict2ndLevelCache() {
try {
    Map<String, ClassMetadata> classesMetadata = sessionFactory.getAllClassMetadata();
        for (String entityName : classesMetadata.keySet()) {
            logger.info("Evicting Entity from 2nd level cache: " + entityName);
            sessionFactory.evictEntity(entityName);
        }
    } catch (Exception e) {
           logger.logp(Level.SEVERE, "SessionController", "evict2ndLevelCache", "Error evicting 2nd level hibernate cache entities: ", e);
     }
}

Внутренности кэша второго уровня

Каждая сущность хранится как CacheEntry и каждая сущность преобразуется в hydrated состояние для создания записи в кэше. В терминах Hibernate, гидратация это когда JDBC ResultSet преобразуется в массив сырых значений:

 
final Object[] values = persister.hydrate( rs, id, object, rootPersister, cols, eagerPropertyFetch, session ); 

То есть он хранит информацию в виде массивов строк, чисел и т. д. И идентификатор объекта выступает указателем на эту информацию. Концептуально это нечто вроде Map, в которой id объекта — ключ, а массивы данных — значение. Приблизительно можно представить себе это так:

 *-----------------------------------------*
 | Person Data Cache |
 |-----------------------------------------|
 | 1 -> [ "John" , "Q" , "Public" , null ] |
 | 2 -> [ "Joey" , "D" , "Public" , 1 ]  |
 | 3 -> [ "Sara" , "N" , "Public" , 1 ]  |
 *-----------------------------------------*

«Гидратированное» состояние сохраняется в текущем запущенном Persistence Context как объект EntityEntry, который инкапсулирует снэпшот сущности, снятую во время загрузки. Затем это состояние используется для:

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

Обратная операция называется дегидратация и она копирует состояние сущности в выражения INSERT и UPDATE.

Как работает кэш запросов (Query cache)?

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

 *----------------------------------------------------------*
 | Query Cache | 
 |----------------------------------------------------------|
 | ["from Person where firstName=?", ["Joey"] ] -> [1, 2] ] |
 *----------------------------------------------------------*

Некоторые запросы не возвращают сущности, вместо них они возвращают только примитивные значения. В этом случае сами значения будут сохранены в кэше запросов. Кэш запросов заполняется когда выполняется кэшируемый JPQL/HQL запрос.

Как связаны кэш второго уровня и кэш запросов?

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

Если кэш второго уровня содержит данные для данного ID, он дегидрирует сущность и вернет ее. Если кэш второго уровня не содержит результаты для данного ID, то тогда SQL запрос будет выполнен для загрузки сущности из БД.

Как включить и настроить кэш второго уровня?

Первый шаг это включить зависимость hibernate-ehcache в вашем pom файле.


<dependenc>
<groupId>org.hibernate</groupId>
 <artifactId>hibernate-ehcache</artifactId>
 <version>SOME-HIBERNATE-VERSION</version>
</dependency>

По умолчанию, кэш второго уровня выключен и для активации его, мы должны выставить следующие свойства Hibernate:


properties.put("hibernate.cache.use_second_level_cache",
      Boolean.TRUE.toString());
properties.put("hibernate.cache.region.factory_class",
      "org.hibernate.cache.ehcache.EhCacheRegionFactory");

или в xml конфигурации:


<prop key="hibernate.cache.use_second_level_cache">true</prop>
<prop key="hibernate.cache.use_query_cache">true</prop>
<prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>
<prop key="net.sf.ehcache.configurationResourceName>your-cache-config.xml</prop>

RegionFactory задает провайдера кэша второго уровня и  свойство hibernate.cache.region.factory_class обязательно к заполнению при выставленном в значение true свойстве hibernate.cache.use_second_level_cache.

Следующим шагом будет сконфигурировать настройки кэша регионов в файле your-cache-config.xml, описание настроек приведено во второй части:


<xml version="1.0">
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" updateCheck="false" xsi:noNamespaceSchemaLocation="ehcache.xsd" name="yourCacheManager">

<diskStore path="java.io.tmpdir"/>

<cache name="yourEntityCache" maxEntriesLocalHeap="10000" eternal="false" overflowToDisk="false" timeToLiveSeconds="86400" />

<cache name="org.hibernate.cache.internal.StandardQueryCache" maxElementsInMemory="10000" eternal="false timeToLiveSeconds="86400" overflowToDisk="false" memoryStoreEvictionPolicy="LRU" />

<defaultCache maxElementsInMemory="10000" eternal="false" timeToLiveSeconds="86400" overflowToDisk="false" memoryStoreEvictionPolicy="LRU" />
</ehcache>

Для включения кэширования на уровне сущностей, мы должны проставить аннотацию над кэшируемой сущностью:


@Entity
@org.hibernate.annotations.Cache(usage =
      CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "yourEntityCache")
public class SomeEntity {
}

Ассоциации так же могут быть закэшированы кэшем второго уровня, но по умолчанию это не включено. Для кэширования ассоциаций нам нужно установить аннотацию @Cache к самой ассоциации:


@Entity
public class SomeEntity {
@OneToMany
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY, region="yourCollectionRegion")
private Set<OtherEntity> other;
}

JPA также предоставляет аннотацию @Cacheable, но она не поддерживает установку свойства concurrency strategy на уровне сущности.

Как использовать кэш запросов?

По умолчанию при кэшировании запросов hibernate  использует два внутренних кэша:

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

 

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


@NamedQuery(name="account.queryName",
     query="select acct from Account ...",
     hints={
        @QueryHint(name="org.hibernate.cacheable",
        value="true")
      }
})

 

И пример как отметить Criteria запрос как кэшируемый:

List cats = session.createCriteria(Cat.class)
 .setCacheable(true)
 .list();

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

(Visited 7 788 times, 1 visits today)

Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.