1.19.2015

13. Hibernate кэш второго уровня.



Выше я рассказывал про hibernate кэш первого уровня, настало время рассказать и про кэш второго уровня. И так коротко, напомню, что кэш первого уровня встроен в hibernate, он всегда включен, его не возможно отключить. Область действия, кэша первого уровня,  в течении действия одной транзакции сессии hibernate, т.е. между вызовами session.beginTransaction() и session.getTransaction().commit().  И так, зачем нам нужен hibernate кэш второго уровня, прежде всего он, уменьшает количество sql запросов к базе данных, увеличивая при этом время отклика программы,  что отражается на её быстродействии. Один раз, попав в кэш второго уровня объект-сущность, используется на всем протяжении жизни объекта sessionFactory, т. е. область действия кэша  - вся наша программа, а если быть точнее, то, как вы настроите свой кэш, так он себя и поведет. В отличие от кэша первого уровня кэш второго уровня нужно включать непосредственно в настройках Hibernate, и он реализуется провайдером,  сторонней библиотекой кэшем. Выбор провайдера зависит от стратегии, которые он может поддерживать, под стратегией понимается, что можно делать над объектом кэша – нашей сущностью: изменять, удалять, вставлять, читать, давайте рассмотрим их:

1. read-only - самая простая стратегия, кэш может только читаться, операции обновления (update) и удаления (delete) не разрешены, однако можно вставлять новые данные (insert) отлично подходит к кэшированию различных справочников,  например наименование регионов, городов, улиц ... и т. д.

2. nonstrict read-write - данные этого кэша могут меняться, возможен конкурентный доступ к одному и тому же объекту. Может произойти ситуация когда в кэше содержатся не последняя измененная сущность, т. е. данные сущности в кэше могут быть не равны данным в базе данных. Отсюда следует, что нужно избегать конкурентного доступа, точнее одни и те же записи не должны  редактировать 2 пользователя. Приведу пример: идеальный случаи это когда кадровики редактируют только свои данные по работникам:  1-й кадровик с 1 по 200 таб. номер, 2-й кадровик с 201 по 400 таб. номер и т. д. Но все же если есть конкурентный доступ к сущности,  то изменения сущности должны  происходить мгновенно. Может возвращать устаревшие данные из кэша.

3. read-write - в целом похож на nonstrict read-write, позволяет более гибко настроить конкурентный доступ, поведение кэша зависит от настройки transaction isolation уровня базы данных, т. е. поведение изменения данных в кэше копирует поведение транзакции.  Максимум, чего можно выжать - это "repeatable read transaction isolation" уровень базы данных. Применяется для не кластерного кэша, используется в основном для предотвращения считывания устаревших  данных с кэша.

4. transactional - изменения в кэше и изменения в базе данных, полностью записываются в одной транзакции. У предыдущих двух стратегий была отложенная запись кэша, т. е. при изменении сущности, с начала, происходит блокировка в кэше (soft locked), после применения транзакции с некоторым опозданием выполняется замена старого значения в кэше, новым.  Уровень этого кэша - это "serializable transaction isolation"  уровень базы данных. Применяется только для распределенных кэшей, в управляемых транзакциях JTA. Полностью исключается чтение устаревших данных из кэша.

Обратите внимание что, чем менее строгую стратегию для кэша вы выбираете, тем большая  производительность  у кэша второго уровня.  Hibernate имеет стратегию кэша по умолчанию, для этого нужно использовать свойство hibernate.cache.default_cache_concurrency_strategy  в файле настроек hibernate.cfg.xml, для примера сделаем  стратегию кэша по умолчанию  read-write:

<property name="hibernate.cache.default_cache_concurrency_strategy">read-write</property>

Рассмотрим  несколько известных кэш провайдеров и посмотрим, какие стратегии они поддерживают.

Провайдер/Стратегия
Read-only
Nonstrict read-write
Read-write
Transactional
EHCache
X
X
X
 X
HashTable (использовать только для тестирования)
X
X
X

Infinispan
X


X

Для лучшего понимания как работает кэш,   привожу рисунок.  


Давайте я вам поясню рисунок,  как происходит поиск сущности в кэше. Но для этого нужно познакомиться с новым понятием. В документации часто встречается понятие инвалидация (invalidated) кэша,  это устаревание данных в кэше,  после того как кэш перешел в это состояние он должен быть очищен от старых данных методом удаления и заново обновлен новыми значениями, после обновления, кэш переходит в  валидное(validate) состояние. Когда сессии hibernate нужно загрузить сущность,  она всегда ищет кэшированную копию сущности в кэше первого уровня.  Если копия сущности существует в кэше первого уровня, то возвращается эта копия, если нет, то поиск продолжается в кэше второго уровня.  Если нашли в кэше второго уровня, то возвращается из кэша второго уровня, но сначала перед этим записывается,  копия сущности в кэш первого уровня.  Соответственно,  если нет копий сущности ни в кэше первого уровня, не  второго уровня, выполняется sql запрос и возвращается сущность из таблицы баз данных, но перед этим,  копия  сущности сохраняется  в кэше первого и второго уровня.  В кэш  второго уровня заносятся все изменения, которые были сделаны в сессии. Кеш второго уровня может стать не актуальным, если  таблицу базы данных редактируют напрямую sql  запросами, в обход hibernate сессии,  в таком случаи нужно обнулять/затирать кэш.   Кэш второго уровня можно сконфигурировать двумя способами.  Первый способ, как кластерный кэш или распределенный кэш который используется двумя или более программами, т.е. программы работают на разных виртуальных машинах (JVM), используя разные “области памяти“, но при этом работают с общим кэшем. Второй способ проще,  используем одну область памяти на одной виртуальной машине (JVM), так сказать sessionFactory кэш.  Кэшировать можно как сущности, так и коллекции сущностей  (many-to-one, one-to-one и т.д.). 
Стоит отметить, что кэш первого уровня и второго уровня применим только к одиночным сущностям, т.е. когда запрашивается  один объект методами  .get() или  .load() сессии, смотрим на пример.

//запрашиваем  1-й раз выполняется  sql запрос
Person  person = (Person) session.get(Person.class, id);
….
//запрашиваем  во второй сессии 1-й раз,  возвращается из кэша, сущность.
Person  person1 = (Person) session1.get(Person.class, id);

 Если выполняется запрос нескольких сущностей, при помощи критериев или языка запросов HQL, то  задействуется кэш запросов,  который так же как и кэш второго уровня, нужно с начала включить в настройках hibernatе и ему так же нужен сторонний кэш провайдер.  Все сторонние кэш провайдеры совместимые с hibernate, реализуют  интерфейс  org.hibernate.cache.spi.CacheProvider,  который нужно указать в файле настроек  hibernate.cfg.xml для свойства hibernate.cache.region.factory_class, для простого понимания привожу таблицу.

Провайдер
Class провайдера
Поддерживает ли кластеры
Поддерживает ли кэш запросов
EHCache
org.hibernate.cache.ehcache.EhCacheRegionFactory
X
 X
HashTable (использовать только для тестирования)
org.hibernate.testing.cache.CachingRegionFactory

 X
Infinispan
org.hibernate.cache.infinispan.InfinispanRegionFactory
 X
X

И так выберем для примера кэш провайдер EHCache, включим поддержку кэша второго уровня и кэш запросов, все это делается в файле настроек hibernate.cfg.xml .

        <! --Включаем кэш второго уровня  -->
        <property name="cache.use_second_level_cache">true</property>
        <! - -Включаем кэш запросов  -->
        <property name="cache.use_query_cache">true</property>
        <property name="cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory
        </property>

Вам нужно знать еще одну вещь, что такое регион кэша, это логическое имя кэша, он нужен для более тщательной и глубокой настройки кэша отдельно взятой сущности. Кеш провайдер EHCache имеет  файл настроек ehcache.xml,  где мы будем настраивать наш кэш,  взглянем на листинг, где приведены  настройки для нашего примера:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="Foo"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd">
    <diskStore path="java.io.tmpdir"/>
    <defaultCache
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="1200"
            timeToLiveSeconds="1200">
    </defaultCache>
    <cache name="org.vit.ch1.Person" maxElementsInMemory="10"
           eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600"
           overflowToDisk="true" memoryStoreEvictionPolicy="LRU"
            />
</ehcache>

Наш кэш может оперировать  данными  в памяти, и по мере необходимости, выгружать их из памяти  на диск, если они не нужны, а когда они снова понадобятся, то подгрузить их с диска.  Элемент  diskStore и его атрибут path указывают, где будут храниться “файлы подкачки” нашего  кэша.  Элемент кэша defaultCache используется по умолчанию для всех, если вы не указали имя региона кэша для сущности, то по умолчанию используются параметры  default кэша. Для более тонкой настройки кэша сущности, используем регион кэша, его логическое имя, оно представлено в элементе cache  атрибутом name.  Взглянем на файл настроек hibernate.cfg.xml, в этом файле посредством элемента class-cache сущности class="org.vit.ch1.Person", сопоставляется регион кэша region="org.vit.ch1.Person", значение для атрибута region совпадает с именем региона кэша  из файла ehcache.xml.  Атрибут usage устанавливает стратегию нашего кэша,  элемент include указывает, следует ли кешировать “ленивые” (lazy) поля сущностей, да – all , или нет - non-lazy.

<mapping class="org.vit.ch1.Person"/>
 <class-cache class="org.vit.ch1.Person"  usage="read-write"  include="non-lazy"
                        region="org.vit.ch1.Person"/>

Описания в файле настроек hibernate.cfg.xml, можно заменить на аннотации для сущности:

@Cache(region = "org.vit.ch1.Person", usage = CacheConcurrencyStrategy.READ_WRITE,  include ="non-lazy")
public class Person implements Serializable {

Нужно знать про правила составления  имен, которым следует придерживаться,  когда мы работаем с кэшем  hibernate.  Правило это простое, имя должно начинаться с имени пакета и заканчиваться его классом, для примера:  org.vit.ch1.Person.  Продолжим, по умолчанию коллекции сущностей не кэшируются,  для их кеширования, нужно явно их описывать с аннотацией @Cache:

package org.vit.ch1
@Cache(region = "org.vit.ch1.Person", usage = CacheConcurrencyStrategy.READ_WRITE,  include ="non-lazy")
public class Person implements Serializable {
@Cache(region = "org.vit.ch1.Person. event ")
private Set<Event> event = …;

Опять же нужно придерживаться правил наименования кэша, иначе можно запутаться, правило описание имени кэша для коллекции простое, имя состоит из имени кэша класса + к нему добавляется имя поля коллекции через  “.”, для нашего примера имя  org.vit.ch1.Person. event.   Давайте опишем основные атрибуты настройки элементов defaultCache и  cache:


Название атрибута.


Его описание.
name
Имя региона кэша. Обязателен для элемента cache.
maxElementsInMemory
Максимальное количество объектов сущностей, которые должны храниться всегда в памяти кэша.
eternal
Могут ли храниться  объекты кэша в памяти ”бесконечно”  долго. – true да, false нет.
timeToIdleSeconds
Промежуток времени обращения к объекту кэша, измеряется в секундах, если в течение этого времени не было обращения к объекту кэша, то сущность выгружается из кэша.  Если поставить значение в 0, то  объект кэша будет находиться в памяти  “бесконечно” долго.
timeToLiveSeconds
Промежуток времени в секундах, в течение которого объект кэша может находиться в кэше, после истечения этого промежутка, объект выгружается из памяти кэша. Если поставить значение в 0, то  объект кэша будет находиться в памяти  “бесконечно” долго.
overflowToDisk
Если превысило количество объектов кэша в памяти больше значения maxElementsInMemory, то выгружать ли данные объектов кэша, на диск в “файлы подкачки”. true да, false нет.
diskExpiryThreadIntervalSeconds
Сколько секунд по времени после помещения  объекта кэша в “файл подкачки”, он может там находиться.
memoryStoreEvictionPolicy
Параметр отвечает за процесс вытеснения объектов кэша, из кэша. Очередь, первым пришел, первым ушел - FIFO. По максимальному времени не использования объекта кэша в кэше – LRU.  По количеству использования объектов кэша, т. е. как часто используется объект в кэше,  и наименьше используемый объект кэша, выгружается из него – LFU.
maxElementsOnDisk
Максимальное количество объектов кэша, которое может содержаться  в “файле подкачки”.
diskPersistent

Допустим, вы перезапускаете свое приложение, нужно ли сохранять кэш на диске и после  перезапуска восстанавливать кэш, true да, false нет.

По умолчанию при кэшировании запросов hibernate  использует два внутренних кэша:  org.hibernate.cache.spi.UpdateTimestampsCache и org.hibernate.cache.internal.StandardQueryCache.  StandardQueryCache  кэш используется, если вы явно не указали в запросе, какой регион/имя кэша использовать, содержит в себе результаты кэшированных запросов.  По умолчанию hibernate  не инвалидирует кэш StandardQueryCache(SQC), т. е. не удаляет его, когда он устаревает или когда были произведены манипуляции с сущностями в кэше, допустим обновление или удаление сущностей.  Тут на помощь приходит кэш UpdateTimestampsCache(UTC), он содержит временные метки (timestamps) последних изменений кэшируемых таблиц.  Когда hibernate нужно выполнить запрос, который был про кэширован,  то он берет метку у кэша UTC  и начинает сравнивать с метками кэша SQC. Если значение у кэша  SQC больше или кэш UTC  не содержит данных для таблицы, то кэш  SQC  считается валидным и данные берутся из него.  При этом нужно учесть, что если кэш UTC пустой, то hibernate  думает что таблица БД не менялась, отсюда следует что, все его объекты должны бесконечно долго находиться в памяти (параметр eternal=”true”) и не должны удалятся из памяти. Если временная метка  SQC кэша меньше, то он считается устаревшим,  происходит выполнение запроса, при этом SQC  кэш не чистится, а заменяется новыми значениями, полученными из таблицы БД.   При изменении сущностей или их удаления, происходит обновление кэша UTC, точнее их временных меток. Это нужно для быстрой работы hibernate, представьте себе такую ситуацию,  у нас используются 20 кэшируемых запросов с разными регионами/именами, и если мы будем по всем кэшам пробегать и менять значения, а прежде чем изменить кэш, его значения нужно  “заблокировать”,  то на всё про всё нам потребуется уйму времени.  А реально некоторые запросы могут выполниться только через  5 или 10 минут, а за это время данные могут смениться 100 раз, и что все сто раз мы будем обновлять эти 20 кэшей? Вот и сделано  так, пока запрос не выполниться, то и SQC кэш не  обновиться, при изменениях отдельных сущностей  обновляются только временные метки кэша UTC.  По  умолчанию запросы и критерии не кэшируются и их нужно включать принудительно методом .setCacheable(true), так же при этом можно указать имя используемого кэша методом .setCacheRegion("org.hibernate.cache.internal.StandardQueryCache”).  На пример:

                // выполняем код в первой сессии
                Query query = session.createQuery("FROM Person p").setCacheable(true);
                query.setCacheRegion("org.hibernate.cache.internal.StandardQueryCache ");
                List<Person> persons = query.list();
               
                /* выполняем код  во второй сессии – уже будет использоваться кэш запросов, если не было ни каких  удалений и изменений  в сущности Person, между двумя вызовами.*/
                Query query1 = session1.createQuery("FROM Person p").setCacheable(true);
                Query1.setCacheRegion("org.hibernate.cache.internal.StandardQueryCache ");
                List<Person> persons1 = query1.list();

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

sessionFactory.getCache().evictQueryRegion("org.hibernate.cache.internal.StandardQueryCache");

или “почистим” кэш сущности, Person с индикатором id:

sessionFactory.getCache().evictEntity(Person.class,id);

На уровне сессии hibernate, или запроса  есть еще один интересный параметр, с которым можно поиграться,  это CacheMode. Настраивается он методом .setCacheMode(), который принимает один параметр типа CacheMode, все возможные варианты приведены в таблице:


Название


Описание функциональности.
CacheMode.NORMAL
Из кэша можно читать данные и писать в него, установлен по умолчанию этот параметр.
CacheMode.GET
Данные могут читаться из кэша, но не могут вносить в него изменения.
CacheMode.PUT
Данные могут писаться в кэш, но не могут читаться из кэша.
CacheMode.REFRESH
Данные могут писаться в кэш, но не могут читаться из кэша, разница между PUT и REFRESH  в том что,  REFRESH  использует параметр настроек hibernate.cache.use_minimal_puts. Включив этот параметр,  Hibernate будет добавлять элементы в кэш, если  только их нет в кэше.   Применяют обычно в  кластерном кэше,  по умолчанию параметр выставлен в “false”.

Комментариев нет:

Отправить комментарий