Grabduck

Кэш Hibernate

:

Одной из основных возможностей Hibernate является использование кэша. При правильной настройке это может давать достаточно большой прирост производительности засчёт сокращения количества запросов к базе данных. Кэш содержит в себе локальную копию данных, которая может сохраняться в памяти либо на диске (имеет смысл, когда приложение и база данных распологаются на разных физических машинах).

Hibernate обращается к кэшу в следующих случаях:

  • Приложение выполняет поиск сущности по идентификатору

  • Приложение выполняет ленивую загрузку коллекции

Кэши разделяются в зависимости от области видимости (scope) на следующие виды:

  • Session scope cache (Кэш привязанный к транзакции, действительный только пока транзакция не завершиться. Каждая транзакция имеет свой кэш, следовательно, доступ к данному кэшу не может быть осуществлён в несколько потоков)

  • Process scope cache (Кэш привязанный к определённому процессу конкретной JVM и общий для многих транзакций с возможностью параллельного доступа)

  • Cluster scope cache (Кэш, общий для нескольких процессов на одной машине или нескольких машин в составе кластера).

По сути, Transaction scope cache представляет собой кэш первого уровня hibernate, кэш же второго уровня может быть реализован либо в области видимости процесса илибо как распределённый кэш.

Общий алгоритм работы кэша hibernate:

  1. Когда сессия пытается загрузить объект в первую очередь он ищется в кэше первого уровня
  2. Если в кэше первого уровня присутствует нужный объект, он возвращается как результат выполнения метода
  3. Если в кэше первого уровня объект не был найден, он ищется в кэше второго уровня
  4. Если объект сохранён в кэше второго уровня, он сохраняется так же в кэш первого уровня и возвращается в качестве результата метода
  5. Если в кэше второго уровня объекта так же не находится, то делается запрос к базе данных, и результат записывается сразу в оба кэша.

Ниже подробенее рассмотрим работу кэша первого и второго уровня.

Кэш первого уровня

Кэш первого уровня в hibernate связан с объектом сессии, он включён по умолчанию и нет возможности отключить его. Когда вы передаёте объект в метод save(), update() или saveOrUpdate(), а так же когда пытаетесь обратиться к нему с помощью методов load(), get(), scroll(), list(), iterate() выполняется добавление элемента в кэш сессии и следующий раз, когда нужно будет произвести повторную выборку данного объекта из БД в текущей сессии обращения к БД уже не произойдёт. Объект будет взят из кэша.

Обнуление кэша происходит после закрытия сессии. Так же, содержимым кэша можно управлять используя методы класса Session:

  • contains() - проверяет сохранён ли объект в кэше

  • flush() - синхронизирует содержимое кэша с базой данных

  • evict() - удаляет объект из кэша

  • clear() - обнуляет кэш

Кэш второго уровня

Кэш второго уровня в hibernate может быть настроен как кэш процесса, так и как распеределённый кэш (в рамках JVM или кластера). В отличие от кэша первого уровня, использование кэша второго уровня является опциональным. Он может быть как включён так и отключён.

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

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

  • read only - используется для данных, которые часто читаются но никогда не изменяются. Изменение данных при использовании этой стратегии приведёт к исключению. Испльзование данного подхода в распределённом кэше позволяет не волноваться об синхронизации данных, однако может привести к сильной загрузке базы на начальном этапе работы кластера. Связано это с тем, что неизменяемые данные необходимы обычно большенству узлов кластера, и каждый из этих узлов будет обращаться напрямую к базе данных, пока не закэширует все необходимые объекты.

  • nonstrict read write - используется для данных, которые изменяются очень редко. При параллельном доступе к данным из разных транзакций данная стратегия не даёт никакой гарантии, что в кэше будут сохранены актуальные данные, так как Hibernate никак не изолирует от других транзакций данные во время изменения. Следовательно, чтение, которое выполняется параллельно изменению может получить доступ к данным, которые ещё не были закоммичены. Не слудует использовать данную стратегию, если небольшая вероятность считывания устаревших данных критична для приложения. При использовании распределённого кэша вероятность работы с устаревшими данными сильно увеличивается, так как устаревшие данные могут быть отражены в кэше многих узлов кластера и сохраняться там достаточно долго (до следующего обновления).

  • read write - используется для данных которые гораздо чаще читаются, чем обновляются, однако, устаревание которых критично для приложения. При этом подходе, данные блокируются для чтения во время их изменении с использованием “soft-lock” механизма. При попытке обратиться к заблокированным данным в данном случае будет произведён запрос к БД. Блокировака будет снята только после коммита. Данная стратегия обеспечивает уровень изоляции транзакций read commited. Использоване этого попхода для распределённого кэша имеет существенные недостатки. Так, если в кластере имеется возможность изменения одних и тех же данных разными узлами, довольно часто могут случаться блокировки устаревших данных в кэше, что сводит на нет приемущества использования данной стратегии. Для того чтобы избавиться от этого нужно использовать кэш-провайдер, поддерживающий блокировки.

  • transactional - используется, когда необходима изоляция транзакций вполоть до уровня repeatable read. Так же как и предыдущие используется для данных, которые гораздо чаще читаются нежели обновляются.

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

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

  • EhCache - изначально разрабатывался как кэш уровня процесса, однако, в последник версиях появилась возможность реализации так же распределённого кэша. Имеется поддрержка кэша запросов и выгрузки данных на диск.

  • OpenSymphony OSCache - реализует кэш только на уровне процесса. Поддерживает кэш запросов и выгрузку данных на диск.

  • SwarmCache - распределённый кэш, который базирется на JGroups. Нет поддержки кэша запросов.

  • JBoss Cache — может быть как локальным так и распределённым. Это полностью транзакционный кэш с возможностью репликации и инвалидаци, а так же с возможностью обмена как синхронными так и асинхронными валидационными сообщениями.

Однако, не каждый провайдер совместим с каждым типом стратегии параллельного доступа (смотри таблицу ниже)

Read-only Nonstrict-read-write Read-write Transactional
EhCache Yes Yes Yes No
OSCache Yes Yes Yes No
SwarmCache Yes Yes No No
Jboss Cache Yes No No Yes

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

<cache name="org.example.model.Entity"
	maxElementsInMemory="500" 
	timeToIdleSeconds="0" 
	timeToLiveSeconds="0" 
	eternal="true" 
	overflowToDisk="false" 
/>

Рассмотрим подробнее каждый из параметров:

  1. name — имя региона кэша
  2. maxElemetsInMemory — максимально возможное число объектов, хранимых в памяти
  3. timeToIdleSeconds — задаёт время жизни объекта в кэше с последнего обращения
  4. timeToLiveSeconds — задаёт время жизни объекта в кэше, начиная с его помещения туда
  5. eternal — атрибут определяющий будут ли элементы удаляться из кэша по истечении таймаута. Если этот атрибут установлен в true, то таймаут будет игнорироваться
  6. overflowToDisk — атрибут, определяющий, будут ли объекты выгружаться на диск при достижении maxElemetsInMemory. Имеет смысл включать, если БД и приложение развёрнуты на разных машинах.

Механизмы синхронизации данных в распределённом кэше

Приложения, которыми пользуется параллельно большое количество людей могут потребовать гораздо больей вычислительной мощности, чем может предоставить единичный сервер. В таком случае, несколько физических машин объеденяются в кластер. Для увеличения общей производительности кластера иногда полезно использовать распределённый кэш hibernate. В таком случае, каждый процесс хранит в своём кэше локальную копию недавно используемых данных. Поддержание актуальности этих данных осуществляется засчёт сетевого взаимодействия процессов с использованием протокола TCP либо UDP.

Механизмы поддержания актуальности данных:

  • копирование (replication) - если состояние сущности было изменнено, то её новое состояние будет разослано каждому члену кластера, что ведёт к повышенному потреблению трафика. А так же принуждает разные узлы кластера сохранять в свём кэше одинаковые данные, что не всегда полезно, так как, обычно, разные члены кластера работают с разными данными.

  • анулирование (invalidation) - если состояние сущности было изменено, то всем узлам кластера рассылается сообщение, указывающее на то, что сущность с определённым идентификатором была изменена. После получения сообщения другие члены кластера проверяют свой кэш на предмет наличия данной сущности, и, если таковая там находися, она удаляется из кэша. Данный подход не создаёт такой большой нагрузки на сеть как предыдущий, а основным его недостатком является то, что если изменённая сущность снова понадобится, она будет загружена запросом к базе данных, а это может привести к дополнительной нагрузке на БД. Однако, данные, которые необходимы многим узлам кластера обычно изменяются очень и очень редко.

Механизмы передачи сообщений о состоянии объектов:

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

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

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

Полезные ссылки: