это перевод Hibernate in Action главы 5
Hibernate транзакции, параллельность и кэширование. Основы транзакций. (Ч1)
Очистка сессии, уровни изоляции транзакций, выбор и установка уровня изоляции транзакций в Hibernate (Ч2)
Пессимистическая и оптимистическая блокировки, транзакции приложения, версионирование, детализация сессии (Ч3)
5.3 Теория и практика кэширования
Основным обоснованием нашего утверждения является то, что приложения, использующие объектно-реляционный слой, как ожидается, превосходят приложения, построенные с использованием прямого JDBC, благодаря возможности кэширования. Хотя мы будем утверждать, что самые странные заявления должны быть сконструированы таким образом, что возможно достигнуть приемлемой производительности без использования КЭШа, нет никаких сомнений, что для некоторых видов приложений – особенно приложения, осуществляющие в основном чтения или приложения, сохраняющие значительные метаданные в БД – кэширование может иметь огромное влияние на производительность.
Мы начнем наше исследование кэширования с некоторой справочной информации. Она включает в себя объяснение различия кэширования и идентичности областей, и влияния кэширования на изоляцию транзакций. Эта информация, и эти правила могут применяться для кэширования в общем; они применимы не только для Hibernate приложений. Эта дискуссия дает основу для понимания, почему система кэширования в Hibernate такая, какая она есть. Затем, мы введем вас в систему кэширования Hibernate и покажем вам, как включить настройки и управлять КЭШем Hibernate первого и второго уровня. Мы рекомендуем вам внимательно изучить основы, изложенные в этом разделе, прежде, чем начать использование кэш-памяти. Без основы, вы можете быстро заиметь сложные в отладки проблемы одновременной доступности и риски целостности ваших данных.
Кэш-память хранит представление текущего состояния БД для приложения, либо в памяти или на диске компьютера сервера приложения. Кэш – это локальная копия данных. Кэш находится между приложением и БД. Кэш может использоваться, чтобы избежать обращений к БД, когда:
• Приложение выполняет поиск по идентификатору (первичному ключу)
• Хранимый слой разрешает ленивую ассоциацию
Также возможно, кэшировать результаты запросов. Как вы увидите, в главе 7, прирост производительности кэширования запросов минимален, большинстве случаев это так, так что эта функция используется гораздо реже.
Прежде, чем мы рассмотрим, как работает Hibernate кэш, давайте рассмотрим различные параметры кэширования и посмотрим, как они относятся к идентичности и параллелизму.
5.3.1 Стратеги кэширования и области
Кэширование является настолько фундаментальным понятием в объектно-реляционном отображении, что вы не сможете понять производительность, масштабируемость или семантику транзакций в реализации объектно-реляционного отображения, без первоначальных представления какую стратегию (или стратегии) кэширования он использует. Существуют три основных типа КЭШа:
•
Транзакционный – связанный с текущей единицей работы, которая может быть фактическая транзакция БД или транзакция приложения. Она корректна и используется во время работы единицы работы. Каждая единица работы имеет свой кэш.
•
Процессный – распределяется между многими (возможно одновременными) единицами работы или транзакции. Это означает, что данные в процессном КЭШе доступны одновременно выполняемым операциям, очевидно с последствиями для изоляции транзакций. Процессный кэш может хранить хранимые объекты целиком в КЭШе, или может хранить их состояние в разобранном формате.
•
Кластерный – распределяется между несколькими процессами на одной машине или между несколькими машинами в кластере. Он требует какой-то удаленный процесс взаимодействия для поддержания согласованности. Кэширование информации должно быть продублировано на всех узлах кластера. Для многих (не всех) приложений, кластерное кэширование имеет сомнительную ценность, так как чтение и обновление КЭШа может быть лишь незначительно быстрее, чем прямое обращение в БД.
Хранимые слои могут обеспечивать несколько уровней кэширования. Например, промах КЭШа(cache miss) (поиск элемента в КЭШе, который там не содержится) в транзакционном типе может последовать за поиском в процессном. Запрос к БД будет последней инстанцией.
Тип КЭШа, используемый хранимым уровнем, влияет на сферу идентичности объектов (взаимосвязь между Java идентичностью и идентичностью на уровне БД)
Кэширование и идентичность объектов
Рассмотрим транзакционный кэш. Кажется естественным, что этот кэш используется также для осуществление идентичности хранимых объектов. Это означает, что транзакционный кэш реализует обработку идентичности: два поиска, использующих один и тот же идентификатор БД, возвращают тот же Java объект в одной единице работы. Поэтому транзакционный кэш идеально подходит, если механизм сохранения также предоставляет транзакционную идентичность.
Механизм сохранения с процессным КЭШем можно выбрать для реализации процессной идентификации. В этом случае, объект идентификации эквивалентен для БД для всего процесса. Два поиска, использующие один и тот же идентификатор БД в двух единицах одновременно выполняемой работы, вернут в результате один и тот же экземпляр Java. Кроме того, объекты извлекаются из процессного КЭШа, могут быть возвращены по значению. Кэш содержит кортежи данных, а не хранимые экземпляры. В этом случае, каждая единица работы загружает свою копию состояния(кортеж) и строит свой собственный хранимый объект. Объем кэш-памяти и объем идентичности объектов уже не являются одинаковыми.
Кластерный кэш всегда требует удаленной связи, а в случае POJO-решений, таких как Hibernate, объекты всегда передаются удаленно по значению. Кластерный кэш не может гарантировать идентичность через кластер. Вы должны выбирать между транзакционной или процессной идентичностью объектов.
Для типичной архитектуры Интернет или корпоративных приложений, самым удобным является, когда сфера идентичности объектов ограничивается одной единицей работы. Иными словами, это ни необходимо, ни желательно, иметь идентичными объекты в двух параллельных потоках. Существуют и другие виды приложений (в том числе некоторые настольные или толстый-клиент архитектуры), где была бы уместно использовать процессную идентичность объектов. Это особенно справедливо, когда память сильно ограничена – потребление памяти транзакционного кэша пропорционально количеству одновременных единиц работы.
Недостатком процессной идентичности является необходимость синхронизации доступа к хранимым объектам в КЭШе, что приводит к высокой вероятности возникновения тупиков.
Кэширование и параллелизм
Любая реализация объектно-реляционного отображения, которая позволяет нескольким единицам работы разделять одни хранимые объекты, должна предоставлять некоторые формы блокировок на уровне объектов, для обеспечения синхронизации параллельного доступа. Обычно, это реализуется, используя блокировки на чтение и запись (устанавливаемые в памяти) вместе с определением тупиков. Реализации, вроде Hibernate, которые поддерживают различных набор экземпляров для каждой единицы работы (транзакционная идентичность), избегают этих проблем в значительной степени.
Наше мнение, что блокировок в памяти следует избегать, по крайней мере для веб-сайтов и корпоративных приложений, где многопользовательская масштабируемость является главной заботой. В этих приложений, обычно нет необходимости сравнивать объекты на идентичность в параллельных единицах работы; каждый пользователь должен быть полностью изолирован от других пользователей.
Существуют довольно веские аргументы в пользу этой точки зрения, когда реляционной СУБД реализует модель многоверсионного параллелизма (например, Oracle или PostgreSQL). Это несколько нежелательно для объектно-реляционного хранимого КЭШа, чтобы переопределять семантику транзакций или модель параллелизма нижележащей БД.
Рассмотрим эту опцию снова. Транзакционный кэш является предпочтительным, если вы также можете использовать транзакционную идентичность объектов и это лучшая стратегия для высоко-параллельных многопользовательских систем. Этот первый уровень КЭШа будет обязательным, посколько он также гарантирует идентичность объектов. Однако, это единственный кэш, который вы можете использовать. Для некоторых данных, может быть полезен кэш второго уровня, относящийся к процессу (или кластеру), который возвращает данные по значению. Поэтому этот сценарий имеет два уровня КЭШа; вы позже увидите, что Hibernate использует этот подход.
Давайте обсудим, какие преимущества есть от второго уровня кэширования, или, другими словами, когда для включать процессный (или кластерный) КЭШ второго уровня, в дополнении к обязательному КЭШу первого уровня.
Кэширование и изоляция транзакций
Процессный или кластерный кэш делают извлечение данных из БД в одной единице работы, видимой для другой единицы работы. Это может иметь очень неприятные эффекты, при изоляции транзакций.
Во-первых, если приложение не имеет эксклюзивного доступа к БД, то процессное кэширование не должно использоваться, за исключением данных, которые редко меняются и могут быть безопасно обновлены по истечению срока кэширования. Этот тип данных часто встречается в приложениях, управляющих содержанием, но редко в финансовых приложениях.
Вам нужно взглянуть на два основных сценария, с участием неэксклюзивного доступа:
• Кластерные приложения
• С общими данными
Любое приложение, задуманное для расширяемости, должно поддерживать кластерные операции. Процессный кэш не поддерживает согласованность между различными КЭШами на разных машинах в кластере. В этом случае вы должны использовать кластерный (распределенный) кэш вместо процессного КЭШа.
Многие Java приложения разделяют доступ к своей БД с другими приложениями. В этом случае, вы не должны использовать любой тип КЭШа, кроме транзакционного кэша. Не существует никакой возможности для кэширующей системы, что знать когда совместное приложение обновило общие данные. Собственно, это можно реализовать на уровне приложения для показа недействительности процессного (или кластерного) КЭШа, когда изменения сделаны в БД, но мы не знаем какого-либо стандартного или лучшего пути достижения этой цели. Конечно, это никогда не будет встроенной возможностью Hibernate. Если вы реализуете такое решения, то оно, скорее всего подойдет только для вас, потому что оно сильно зависит от среды и используемых продуктов.
После рассмотрения неэкслюзивного доступа к данным, вы должны установить, как уровень необходим для данных приложения. Не всякий кэш реализует все уровни изоляции транзакций и это критически важно узнать. Давайте посмотрим на данные, которые выгодны для процессного (или кластерного) КЭШа.
Полное объектно-реляционное решение позволит вам настроить кэш второго кровня отдельно для каждого класса. Хорошими кандидатами на кэширования являются классы, которые представляют:
• Данные, которые редко меняются
• Не критичные данные
• Данные, которые локальны для приложения и не являются общими
Плохими кандидатами для КЭШа второго уровня являются:
• Данные, которые часто меняются
• Финансовые данные
• Данные, которые являются общими с другими приложениями
Однако, это не все правила, которые мы обычно применяем. Многие приложения имеют несколько классов, со следующими свойствами:
• Маленькое количество экземпляров
• На каждый экземпляр ссылается много других экземпляров другого класса или классов
• Экземпляры редко (или никогда) обновляются
Такие данные иногда называют справочными данными. Справочные данные являются отличным кандидатом для кэширования процессным или кластерным КЭШем, и любое приложение, которое интенсивно использует эти данные сильно выигрывает от того, что данные закэшированы. Вы позволяете данным обновляться во время истечение таймаута КЭШа.
Мы сформировали картину двойного слоя системы кэширования в предыдущих разделах, с транзакционным КЭШем первого уровня и опциональным процессным или кластерным КЭШем второго уровня. Это похоже на систему кэширования Hibernate.
5.3.2 Архитектура КЭШа в Hibernate
Как мы говорили ранее, Hibernate имеет двухуровневую архитектуру кэш-памяти. Различные элементы этой системы можно увидеть на рис. 5.5
(РИС. 5.5)
Кэш первого уровня является также сессией. Жизненный цикл сессии соответствует либо транзакции БД, либо транзакции приложения (как описано ранее в этой главе). Мы считаем кэш, связанный с сессий транзакционным. Кэш первого уровня является обязательным и не может быть выключен; это также гарантирует идентичность объектов внутри транзакции.
Кэш второго уровня в Hibernate является подключаемым и может быть процессным или кластерным. Это кэш состояний (возвращаемых по значению), а не хранимых экземпляров. Стратегия параллельности КЭШа определяет детали транзакционной изоляции для определенных элементов данных, в то время, как поставщик КЭШа представляет собой физическую, фактическую реализацию КЭШа. Использование КЭШа второго уровня является необязательным и может быть настроено на каждый класс и на каждую ассоциацию.
Hibernate также реализует кэширование результатов запросов, тесно интегрированных с КЭШем второго уровня. Это является дополнительной функцией. Мы обсуждаем кэш запросов в главе 7, так как его использование тесно связано с фактическим выполнением запросов.
Давайте начнем с использования КЭШа первого уровня, который также называется КЭШем сессии.
Использование КЭШа первого уровня
Кэш сессии гарантирует, что когда приложение запросит такой же хранимый объект дважды в той же сессии, то он получит тот же (идентичный) Java экземпляр. Это иногда позволяет избегать ненужного трафика БД. Более важно, что он обеспечивает следующее:
• Слой хранимых объектов не подвержен опасности переполнения стека, в случае циклических ссылок в графе объектов
• Никогда не возникнет конфликтов представлений об одной строке БД в конце транзакции БД. Существует не более одного объекта, представляющего любую строку БД. Все изменения, внесенные в этот объект, могут быть безопасно записаны в БД (сброшены).
• Изменения, сделанные в конкретной единице работы всегда незамедлительно видны всему другому коду, исполняемому внутри этой единицы работы.
Вам не нужно делать ничего специального для того, чтобы разрешить кэш сессии. Он всегда включен, и по указанным причинам, не может быть отключен.
Всякий раз, когда вы вызываете
save(),
update() или
saveOrUpdate() и всякие раз, когда вы получаете объект, используя
load(),
find(),
list(),
iterate() или
filter(), он добавляется в кэш сессии. Когда впоследствии вызывается
flush(), то состояние объекта синхронизируется с БД.
Если вы не хотите, чтобы эта синхронизация происходила, или если вы обрабатываете огромное количество объектов и необходимы эффективные средства управления памятью, то вы можете использовать метод
evict() сессии, для удаления объекта и его коллекции из первого уровня кэш-памяти. Есть несколько сценариев, где это может быть полезно.
Управление КЭШем первого уровня
Рассмотрим этот часто задаваемый вопрос: «Я получаю OutOfMemoryException при попытке загрузить 100,000 объектов и манипулируя всеми ими. Как я могу сделать массовые обновления с Hibernate?».
Наше мнение, что объектно-реляционное отображение не подходит для операций массового обновления (или удаления). Если у вас есть прецедент, как этот, то иная стратегия почти всегда лучше: вызов хранимой процедуры в БД или использование прямых SQL UPDATE и DELETE выражений. Не передавайте все данные в основную память для простых операций, если она может быть выполнена более эффективно в БД.
Если ваше приложение в основном использует массовые операции, то объектно-реляционного отображение не является правильным инструментом для работы!
Если вы настаиваете на использовании Hibernate для массовых операций, вы должны немедленно вызывать
evict() каждого объекта, после того, как он будет обработан (при переборе результатов запроса), и тем самым предотвратить исчерпание памяти.
Чтобы полностью убрать все объекты из КЭШа первого уровня, вызовите
Session.clear(). Мы не пытаемся убедить вас, что убранные объекты первого уровня является плохой вещью в целом, однако, редко встречаются хорошие случаи его использования. Иногда использование проекций и отчетных запросов, как это обсуждается в главе 7, раздел 7.4.5 «Повышение производительности с отчетными запросами», может быть лучшим решением.
Обратите внимание, что убирание, как и операции сохранения и удаления, могут автоматически применяться к ассоциированным объектам. Hibernate будет убирать ассоциированные экземпляры из сессии, если в файле отображения атрибуту cascade задано значение
”all” или
“all-delete-orphan” для конкретной ассоциации.
Когда случается промах первого уровня КЭШа, Hibernate пытается взять значение из КЭШа второго уровня, если он включен для определенного класса или ассоциации.
Кэш второго уровня Hibernate
Hibernate кэш второго уровня является процессным или кластерным; все сессии обладают одним и тем же КЭШем второго уровня. Кэш второго уровня, на самом деле, имеет область
SessionFactory.
Хранимые экземпляры хранятся в КЭШе второго уровня в разобранном виде. Думайте о демонтаже, как о процессе похожем на сериализацию (однако, алгоритм, намного, намного быстрее, чем Java сериализация).
Внутреннее представление этого процессного/кластерного КЭШа не представляет большого интереса; более важным является правильное использование политики кэширования, то есть стратегии кэширования и выбор физических провайдеров КЭШа.
Различные виды данных требуют различной политики кэширования: соотношение чтение к записи изменяется, размер БД изменяется, и некоторые таблицы используются совместно с другими приложениями. Так что, кэш второго уровня настраиваемся под детализацию каждого индивидуального класса или коллекцию ролей. Это позволяет, например, разрешить кэш второго уровня для справочных данных и запретить его для классов, представляющих финансовые записи. Политика КЭШа включает в себя настройку следующих параметров:
• Включен ли кэш второго уровня
• Стратегию параллелизма Hibernate
• Политика истекания срока кэширования (такую, как тайм-аут, LRU, зависимую от ОП)
• Физическое устройство КЭШа (в памяти, индексируемые файлы, кластерная репликация)
Не все классы имеют выгоду от кэширования, поэтому чрезвычайно важно иметь возможность отключить кэш второго уровня. Повторяем, кэш-память, как правило полезно только для классов, которые по большинству считываются. Если у вас есть данные, которые обновляются чаще, чем читаются, не разрешайте кэш второго уровня, даже если все остальные условия для кэширования верны! Кроме того, кэш второго уровня может быть опасен в системах, которые разделяют данные с другими приложениями, которые могут эти данные изменить. Как мы объяснили в предыдущих разделах, необходимо тщательно подумать над решением.
Hibernate кэш второго уровня устанавливается в два этапа. Во-первых, вы должны решить, какую стратегию параллельности использовать. После этого вам нужно настроить истечение КЭШа и физические атрибуты, используя поставщика КЭШа.
Встроенные стратегии параллелизма
Стратегия параллелизма является посредником; она несет ответственность за хранение элементов данных в кэш памяти и извлечение их из КЭШа. Это важная роль, поскольку она определяет семантику изоляции транзакций для этого конкретного пункта. Вам придется принять решение по каждому хранимому классу, какую стратегию параллелизма кэша использовать, если вы хотите разрешить кэш второго уровня.
Есть четыре встроенных стратегии параллелизма, представляющие снижение уровня строгости, в терминах изолированности транзакции:
•
Транзакционная – доступна только в управляемой среде. Она гарантирует полную изоляцию транзакций до повторяемого чтения, если это требуется. Используйте эту стратегию для данных которых в большинстве считываются, в которых очень важно предотвратить появление устаревших данных в параллельных транзакциях, в редких случаях обновления.
•
Чтение-запись – поддерживает изоляцию чтения подтвержденного, используя механизм временных меток. Она доступна только в некластерных средах. Опять же, использовать эту стратегию нужно для чтения данных, в которых очень важно предотвратить появление устаревших данных в параллельных транзакциях, в том редком случае обновления.
•
Нестрогое-чтение-запись - не дает никакой гарантии согласованности между КЭШем и БД. Если есть возможность одновременного доступа к одной сущности, то вам необходимо настроить достаточно короткий срок истечения тайм-аута. В противном случае, вы можете прочитать устаревшие данные в КЭШе. Используйте эту стратегию, если данные редко меняются (несколько часов, дней или даже недель), и небольшая вероятность появления устаревших данных не является критической проблемой. Hibernate считает недействительным кэшируемый элемент, если модифицируемый объект очищается(
flush), но это асинхронные операции, без какой-либо блокировки КЭШа или гарантии, что полученные данные являются последней версией.
•
Только-для-чтения – данная стратегия подходит для данных, которые никогда не меняются. Используйте её только для справочных данных.
Обратите внимание, что с уменьшением строгости приходит увеличение производительности. Вы должны тщательно оценивать эффективность кластерного КЭШа с полной изоляцией транзакций, перед тем как использовать его в реальных условиях. Во многих случаях, вам было бы лучше, отключить кэш второго уровня для конкретного класса, если устаревшие данные не приемлемы. В начале, протестируйте ваше приложение с отключенным КЭШем второго уровня. Тогда включайте его только для хороших классов-кандидатов, по одному, постоянно тестируя производительность вашей системы и оценивая стратегию параллелизма.
Можно определить свою собственную стратегию, реализуя
net.sf.hibernate.cache.CacheConcurrencyStrategy, но это довольно трудная задача и применяется только в крайне редких случаях оптимизации.
Вашим следующим шагом после выбора стратегий параллелизма, которые вы будете использовать для ваших кандидатов на кэширования, является выбор поставщиков КЭШа. Поставщик является подключаемым, физической реализаций системы кэширования.
Выбор поставщика КЭШа
Сейчас, Hibernate заставляет вас выбирать одного поставщика КЭШа для всего приложения. Следующие поставщики встроены в Hibernate:
• EHCache предназначен для простого процессного кэширования в одной JVM. Он может кэшировать в памяти или на диске и поддерживает опциональный Hibernate кэш результатов запроса.
•
OpenSymfony OSCache – это библиотека, которая поддерживает кэширование в памяти и на диске в одной JVM, с богатым набором политик истечения и поддержкой КЭШа запросов.
•
SwarmCache – это кластерный кэш, основанный на JGroups. Он использует кластерное аннулирование, но не поддерживает кэш Hibernate запросов.
•
JBossCache – это полностью транзакционно-репликационный кластеризованный кэш, также основанных на
JGroups. Кэш запросов Hibernate поддерживается, предполагая, что часы в кластере синхронизированы.
Легко написать адаптер для других продуктов, реализуя
net.sf.hibernate.
cache.CacheProvider.
Не каждый поставщик КЭШа совместим с каждой стратегией параллелизма. Матрица совместимости представлена в таблице 5.1 поможет вам выбрать соответствующую комбинацию.
Поставщик кэша
| только-для-чтения
| нестрогое-чтение-запись
| чтение-запись
| транзакционная
|
EHCache
| x
| x
| x
| |
OSCache
| x
| x
| x
| |
SwarmCache
| x
| x
| | x
|
JBossCache
| x
| | | x
|
Настройка кэширования включает в себя два этапа:
1. Взгляните на файлы отображения ваших хранимых классов и решите, какую стратегию параллельного КЭШа вы хотели бы использовать для каждого класса и для каждой ассоциации.
2. Включите предпочтительного поставщика КЭШа в глобальной конфигурации Hibernate и установите конкретные настройки поставщика.
Например, если вы используете OSCache, нужно отредактировать oscache.properties, или для EHCache ehcache.xml в ваших путях к классам(classpath).
Давайте, добавим кэширование к нашему CaveatEmptor Category и Item классам.
Hibernate кэширование на практике и заключение (Ч5)