это перевод книги Hibernate In Action Глава 5
Hibernate транзакции, параллельность и кэширование. Основы транзакций. (Ч1)
Очистка сессии, уровни изоляции транзакций, выбор и установка уровня изоляции транзакций в Hibernate (Ч2)
Hibernate транзакции, параллельность и кэширование. Основы транзакций. (Ч1)
Очистка сессии, уровни изоляции транзакций, выбор и установка уровня изоляции транзакций в Hibernate (Ч2)
5.1.7 Использование пессимистической блокировки
Блокировка – это механизм, который предотвращает одновременный доступ к конкретному объекту данных. Когда одна транзакция владеет блокировкой на элемент, то непараллельная транзакция может читать и/или изменять этот элемента. Блокировка может быть только моментальным замком, состоявшимся в том время, как элемент был прочитан, или он может состояться до тех пор пока транзакция не завершится. Пессимистическая блокировка – это блокировка, которая устанавливается при чтении элемента и удерживается до тех пор, пока транзакция не завершится.
В режиме чтения подтвержденного (наш предпочтительный уровень изоляции транзакций), БД никогда не приобретает пессимистическую блокировку, если это явно не запрашивается приложением. Как правило, пессимистические блокировки не являются самым масштабируемым подходом к параллелизму. Однако, при определенных обстоятельствах, они могут быть полезны для предотвращения тупиковых ситуаций на уровне БД, которые приводят к сбою транзакций. Некоторые СУБД (например, Oracle и PostgreSQL) обеспечивают SQL SELECT … FOR UPDATE синтаксис, чтобы разрешить использование явных пессимистических блокировок. Вы можете проверить Hibernate диалекты, чтобы узнать поддерживает ли ваша БД эту функцию. Если БД не поддерживает такой функции, то Hibernate будет всегда выполнять обычный SELECT без FOR UPDATE пункта.
Класс Hibernate LockMode позволяет запрашивать пессимистические блокировки на конкретный элемент. Кроме того, вы можете использовать LockMode, чтобы заставить Hibernate не использовать уровень КЭШа или выполнить простую проверку версии. Вы увидите выгоду этих операций, когда мы обсудим версионирование и кэширование.
Давайте взглянем на использование LockMode. Если у вас есть транзакция, на подобии этой:
Transaction tx = session.beginTransaction();
Category cat = (Category) session.get(Category.class, catId);
cat.setName("New Name");
tx.commit();
тогда, вы можете установить пессимистичную блокировку следующим образом:
Transaction tx = session.beginTransaction();
Category cat = (Category) session.get(Category.class, catId, LockMode.UPGRADE);
cat.setName("New Name");
tx.commit();
В этом режиме, Hibernate будет загружать категории, используя SELECT … FOR UPDATE, таким образом, блокирую возвращаемые строки в БД, пока они не будут освобождены, когда транзакция завершится.
Hibernate определяет несколько стратегий блокировок:
• LockMode.NONE – не обращается к БД, за исключением случае, если объект не в КЭШе.
• LockMode.READ – обходит оба уровня КЭШа, а также выполняет проверки версии, чтобы убедиться, что версия объекта в памяти совпадает с той, что существует в БД.
• LockMode.UPGRADE – обходит оба уровня КЭШа, выполняет проверку версии (если применимо), и получает пессимистическую блокировку обновления уровня БД, если она поддерживается.
• LockMode.UPGRADE_NOWAIT – тоже самое, что UPGRADE, но использует SELECT … FOR UPDATE NOWAIT в Oracle. Это отключает ожидание освобождения параллельных блокировок, тем самым выбрасывая исключение о блокировке немедленно, если блокировка не может быть получена.
• LockMode.WRITE – устанавливается автоматически, когда Hibernate записывает строку в текущей транзакции (это внутренний режим; вы не можете указать это явно).
По умолчанию, load() и get() используют LockMode.NONE. LockMode.READ является наиболее полезным с Session.lock() и несвязанным объектом. Например:
Item item = ... ;
Bid bid = new Bid();
item.addBid(bid);
...
Transaction tx = session.beginTransaction();
session.lock(item, LockMode.READ);
tx.commit();
Этот код выполняет проверку версии на несвязанном Item для проверки того, что строка БД не обновлена другой транзакцией с момента её извлечения, перед сохранением нового Bid каскадно (предполагая, что каскадная ассоциация от Item к Bid разрешена).
Задавая явный LockMode, отличный от LockMode.NONE, вы заставляете Hibernate обойти оба уровня КЭШа и пройти весь путь к БД. Мы считаем, что в большинстве случаев кэширование является более полезным, чем пессимистическая блокировка, поэтому мы не используем явный LockMode, если мы действительно в нем не нуждаемся. Наш совет в том, что если у вас есть профессиональный администратор БД на вашем проекте, то пусть администратор решит какие операции требуют пессимистических блокировок, как только приложение запущено и работает. Это решение должно зависеть от тонких деталей взаимодействия между различными операциями, о которых можно не догадаться сразу.
Давайте рассмотрим ещё один аспект одновременного доступа к данным. Мы считаем, что большинство Java разработчиков знакомы с понятием транзакции БД, и это то, что они обычно имеют ввиду под транзакцией. В этой книге, мы полагаем, что это мелкозернистые(fine-grained) транзакции, но мы также полагаем более крупнозернистое понятие. Наши крупнозернистые транзакции будут соответствовать тому, что пользователь считает одной единицей работы. Почему они должны отличаться от мелкозернистых транзакций БД?
БД изолирует воздействие параллельных транзакций БД. Она должна появиться в приложении, где каждая транзакция является единственной доступной транзакцией в настоящее время в БД (даже если это не так). Изоляция является дорогостоящей. БД должна выделять значительные ресурсы для каждой транзакции на время транзакции. В частности, мы уже обсуждали, что многие БД блокируют строки, которые были прочитаны или обновлены в транзакции, не допуская любые другие транзакции, до завершения первой транзакции. В системах с высокой степенью параллелизма, эти блокировки могут ухудшить масштабируемость, если они удерживаются дольше, чем это абсолютно необходимо. По этой причине, вы не должны удерживать транзакции БД (или даже JDBC соединений) открытых, в ожидании пользовательского ввода (все это разумеется также относится к Hibernate транзакции, поскольку это всего лишь адаптер к основном механизму транзакций БД).
Если вы хотите работать с длительным ожиданием пользовательских действий, используя преимущества ACID транзакций, то простые транзакции БД не являются достаточными. Вам нужна новая концепция, длительные транзакции приложения.
5.2 Работа с транзакциями приложения
Бизнес-процессы, которые могут рассматриваться, как одна единица работы с точки зрения пользователя, обязательно охватывают несколько клиентских запросов. Это особенно справедливо, когда пользователь принимает решение об обновлении данных на основе текущего состояния этих данных.
В крайнем примере, предположим, что вы собираете данные, введенные пользователем на нескольких экранах, возможно, с помощью пошагового мастера. Вы должны прочитать и записать соответствующие пункты данных в нескольких запросах (следовательно, несколько транзакций БД) пока пользователь не нажмет «Готово» на последней странице. На протяжении всего этого процессы, данные должны оставаться согласующимися и пользователь должен быть проинформирован о любых изменениях в данных, сделанных в любой параллельной транзакции. Мы называем это крупнозернистой концепцией транзакции приложения, более широкое понятие единицы работы.
Мы сейчас переформулируем это определение точнее. Большинство веб-приложений включает в себя несколько примерно следующих типов функциональности.
1. Данные извлекаются и отображаются на экране в первой транзакции БД.
2. Пользователь имеет возможность просмотреть и изменить данные, вне всяких транзакций БД.
3. Изменения сохраняются в БД во второй транзакции.
В более сложных приложения, это может быть несколько таких взаимодействий с пользователем до того, как конкретное бизнес-действие будет завершено. Это приводит к понятию транзакции приложения (иногда, называемое, длинные транзакции, пользовательская транзакция или бизнес транзакция). Мы предпочитаем понятие транзакция приложения или пользовательская транзакция, так как эти условия являются менее расплывчатыми и подчеркивают аспект транзакции с точки зрения пользователя.
Поскольку вы не можете рассчитывать на БД для обеспечения изоляции (или даже атомарности) от параллельных транзакций приложения, изоляция становится заботой самого приложения, возможно даже озабоченностью пользователя.
Давайте обсудим применение транзакций приложения на примере.
В нашем приложении CaveatEmptor, как пользователь может размещать комментарий, так и любой администратор может открыть экран редактирования комментариев, удалять или редактировать текст комментария. Предположим, что два разных администратора открыли экран редактирования, чтобы посмотреть один и тот же комментарий одновременно. Как изменить текст комментария и отобразить эти изменения. На данные момент, мы имеем три способа справиться с попыткой одновременной записи в БД:
• Последний коммит выигрывает - оба обновления являются успешными, а второе изменение перезаписывает изменения первого. Сообщение об ошибке не показывается.
• Первый коммит выигрывает – первая модификация записывается, а пользователь, предоставивший второе изменение, получает сообщение об ошибке. Пользователь должен перезапустить бизнес-процесс, получая обновленные комментарии. Этот вариант часто называют оптимистической блокировкой.
• Слияние конфликтующих обновлений – первая модификация записывается, а вторая может применяться пользователем по выбору.
Первый вариант, где последний выигрывает, проблематичен; второй пользователь перезаписывает изменения первого пользователя, не видя изменений, внесенных первым пользователем, даже зная, что они существуют. В нашем примере, это вероятно, не имело бы значения, но это было бы неприемлемо для некоторых других видов данных. Второй и третий варианты, как правило приемлемы для большинства видов данных. С нашей точки зрения, третий вариант, это просто изменений второго – вместо того, чтобы показывать сообщение об ошибке, мы показываем сообщение, а затем позволяем пользователю вручную объединить изменения. Не существует единственного наилучшего решения. Вы должны исследовать собственные бизнес-требования, чтобы выбрать между этими тремя вариантами.
Первый вариант бывает по умолчанию, если вы не делаете ничего особенного в вашем приложении, поэтому этот вариант не требует никаких усилий с вашей стороны (или на стороне Hibernate). Вы будете иметь две транзакции БД: данные комментария загружается в первой транзакции, а вторая транзакция сохраняет изменения в БД, без проверки наличия обновлений, что могло произойти.
С другой стороны, Hibernate может помочь вам реализовать вторую и третью стратегию, с помощью управляемого версионирования для оптимистической блокировки.
5.2.1 Использование управляемого версионирования
Управляемой версионирование основывается либо на номере версии, который увеличивается или на метке, которая обновляется в настоящем времени, при любом изменении объекта. Для управляемого версионирования в Hibernate, мы должны добавить новое свойство для нашего класса Comment и отобразить его в качестве номера версии, используя тег . Во-первых, давайте посмотрим на изменения в классе Comment:
public class Comment {
...
private int version;
...
void setVersion(int version) {
this.version = version;
}
int getVersion() {
return version;
}
}
Вы можете также использовать модификаторы public для сеттера и геттера. Свойство должно прийти сразу после отображения идентификатора в файле отображения для класса Comment:
<class name="Comment" table="COMMENTS">
<id ...
<version name="version" column="VERSION"/>
...
</class>
Номер версии – это просто счетчик, он не имеет какой-либо полезного семантического значения. Некоторые люди предпочитают использовать временные метки(timestamp), вместо счетчика:
public class Comment {
...
private Date lastUpdatedDatetime;
...
void setLastUpdatedDatetime(Date lastUpdatedDatetime) {
this.lastUpdatedDatetime = lastUpdatedDatetime;
}
public Date getLastUpdatedDatetime() {
return lastUpdatedDatetime;
}
}
<class name="Comment" table="COMMENTS">
<id ...../>
<timestamp name="lastUpdatedDatetime" column="LAST_UPDATED"/>
...
</class>
Теоретически, временные метки немного менее безопасны, так как две параллельные транзакции могут одновременно загружать и обновлять один элемент в одну и ту же миллисекунду; на практике это вряд ли произойдет. Тем не менее, мы рекомендуем, чтобы в новых проектах использовались числовая версия, а не временные метки.
Вам не нужно устанавливать значение версии или временной метки самому; Hibernate будет инициализировать значение, когда вы впервые сохраните Comment и увеличит и сбросит его при каждом изменении объекта.
FAQ Если версия родительского объекта изменится, то изменится ли наследник? Например, если одна ставка в коллекции ставок Item изменится, то изменится ли номер версии для Item или нет? Ответ на этот и подобные вопросы прост: Hibernate будет увеличивать номер, когда объект «загрязнен»(dirty). Это включает в себя все «грязные» свойства, будь то одиночные значения или коллекции. Подумайте о взаимоотношениях Item и Bid: если изменяется Bid, то версия связанного Item не увеличивается. Если мы добавим или удалим Bid из коллекции ставок, то версия Item будет обновлена (конечно, мы хотели бы сделать Bid неизменяемым классом, поскольку не имеет смысла изменять ставки).
Всякий раз, когда Hibernate обновляет комментарий, он использует колонку с версией в SQL WHERE условии:
update COMMENTS set COMMENT_TEXT='New comment text', VERSION=3
where COMMENT_ID=123 and VERSION=2
Если другая транзакций приложения будет обновлять этот же элемент, то поскольку он был считан в текущей транзакции приложения, столбец с версией не будет содержать значение 2, и строка не будет обновлена. Hibernate будет проверять количество строк, возвращаемых JDBC драйвером –которые в данном случае будут обозначать количество обновляемых строк, если их количество будет равно нулю, то будет выброшено исключение StaleObjectStateException.
Используя это исключение, мы можем показать пользователю второй транзакции приложения сообщение об ошибке («Вы работали с устаревшими данными, поскольку другой пользователь изменил их!»), и пусть первая транзакция выиграет. Кроме того, мы могли бы поймать исключение и показать второму пользователю новый экран, позволяющий пользователю вручную произвести слияние между двумя версиями.
Как вы можете видеть, Hibernate позволяет с легкостью использовать версии для осуществления оптимистической блокировки. Можете ли вы использовать оптимистические и пессимистические блокировки вместе, или вы можете принять решения только в пользу одной блокировки? А почему она называется оптимистической?
Оптимистический подход всегда предполагает, что все будет хорошо, и что конфликтующие изменения данных происходят редко. Вместо того, чтобы быть пессимистичным и блокировать одновременный доступ к данным сразу же (и форсируя сериализацию выполнение), оптимистический контроль параллелизма будет блокировать только в конце единицы работы и выбрасывать исключение.
Конечно, обе стратегии имеют свои области применения. Многопользовательские приложения обычно по умолчанию используют оптимистический контроль параллелизма и используют пессимистические блокировки, когда это необходимо. Обратите внимание, что продолжительность пессимистической блокировки в Hibernate является продолжительность одной транзакции БД! Это означает, что вы не можете использовать эксклюзивную блокировку, чтобы заблокировать одновременный доступ дольше, чем на одну транзакцию БД. Мы считаем, что это хорошо, потому что единственным решением была бы чрезвычайно дорогая блокировка в памяти (или так называемая блокировка таблицы в БД) на срок, например, транзакции приложения. Это почти всегда узкое место в производительности; каждый доступ к данным требует дополнительной проверки блокировки с синхронизированным менеджером блокировок. Вы можете, если это сильно необходимо в вашем конкретном приложении, осуществлять простую длительную пессимистичную блокировку, используя Hibernate для управления блокированием таблицы. Шаблоны для этого могут быть найдены на сайте Hibernate, но мы определенно не рекомендуем такой подход. Необходимо внимательно изучить последствия этого исключительного случая.
Давайте вернемся к транзакциям приложения. Теперь вы знаете основы управляемого версионирования и оптимистической блокировки. В предыдущих главах (а ранее в этой главе), мы говорили о Hibernate-сессии, как не о том, что является транзакцией. В самом деле, сессия имеет большую гибкость, и её можно использовать по-разному с БД и транзакциями приложения. Это означает, что степень детализации(гранулированности) является гибкой; это может быть любая единица работы, по вашему желанию.
5.2.2 Детализации сессии
Чтобы понять, как можно использовать Hibernate-сессию, давайте рассмотрим отношения между транзакциями. Ранее, мы обсуждали две взаимосвязанные концепции:
• Уровень идентичности (см. раздел 4.1.4)
• Детализацию БД и транзакций приложения
Hibernate сессия определяет сферу идентичности объекта. Транзакция Hibernate соответствует сфере транзакций БД.
(РИСУНОК 5.2)
Какова взаимосвязь между сессиями и транзакциями приложения? Давайте начнем обсуждение с наиболее распространенным использованием сессии.
Обычно, мы открываем новую сессию для каждого запроса клиента (например, запроса веб-браузера) и начинаем новую транзакцию. После выполнения бизнес-логики, мы фиксируем изменений транзакции БД и закрываем сессию перед отправкой ответа клиенту (см. рис. 5.2).
Сессии (S1) и транзакции БД (T1) имеют одну и ту же степень детализации. Если вы работаете с концепцией транзакций приложения, то это простой подход – все что вам нужно в вашем приложении. Нам нравится называть такой подход сессия-на-запрос.
Если вам необходима длительная транзакция приложения, то вам могут помочь несвязанные объекты (и Hibernate поддерживает оптимистическую блокировку, обсуждаемую в предыдущем разделе), осуществите его, используя тот же подход (см. рис. 5.3).
Предположим, что ваша транзакция приложения охватывает два клиентских запроса/ответа, например два HTTP-запроса в веб-приложении. Вы можете загрузить интересующие объекты в первой сессии, а затем связать их к новой сессии после того, как они были изменены пользователем. Hibernate автоматически проверит версию. Время между (S1, T1) и (S2, T2) может быть «длинным», настолько длинным, насколько это нужно пользователю, чтобы сделать его изменения. Этот подход известен также, как сессия-на-запрос-с-несвязанными-объектами.
Кроме того, вы можете предпочесть использовать одну сессию, которая охватывает несколько запросов вашей транзакции приложения. В этом случае, вам не нужно беспокоиться о присоединении несвязанных объектов, так как объекты остаются хранимыми в рамках одной длительной сессии (см. рис. 5.4). Конечно, Hibernate по-прежнему несет ответственность за выполнение оптимистической блокировки.
Сессия сериализуемая и может быть безопасно сохранена, например, в сервлете HttpSession. Конечно, базовое JDBC соединение должно быть закрыто и новое соединение должно быть получено на следующий запрос. Вы используете методы disconnect() и reconnect() интерфейса сессии для того, чтобы освободить соединение и затем получать новое соединение. Такое подход известен, как сессия-на-транзакцию-приложения или длинная сессия.
Как правило, ваш первый выбор должен сохранять Hibernate сессию открытой не более, чем на одну транзакцию БД (сессия-на-запрос). После завершения первоначальной транзакции БД, чем дольше сессия остается открытой, тем больше шансов, что она удерживает устаревшием данные в КЭШе хранимых объектов (кэш первого уровня является обязательным в сессии). Конечно, вы никогда не должны повторно использовать одну сессию дольше, чем это требуется для завершения одной транзакции приложения.
Вопрос о реализации транзакций приложения и объем сессии важен при построении приложения. Мы обсудим реализацию стратегии, с примерами в главе 8 «Реализация транзакций приложения».
В заключении, существует важный вопрос, которым вы можете быть обеспокоены. Если вы работаете с предоставленной схемой БД, то вы, вероятно, не можете добавить колонки версию или временных отметках для оптимистической блокировки в Hibernate.
5.2.3 Другие способы реализации оптимистической блокировки
Если у вас не колонок с версией или временной отметки, то Hibernate может выполнять оптимистическую блокировку, но только для объектов, которые извлечены и изменены в той же сессии. Если вам необходима оптимистическая блокировка для несвязанных объектов, то вы должны использовать номер версии или временные метки.
Это альтернативная реализация оптимистической блокировки проверяет текущее состояние БД, на неизменность значений хранимые свойств, в то время, как объект был извлечен (или во время последней очистки сессии). Вы можете включить эту функцию, установив optimistic-lock атрибут в файле отображения класса:
<class name="Comment" table="COMMENT" optimistic-lock="all">
<id ...../>
...
</class>
Сейчас, Hibernate будет включать все свойства в WHERE условие:
update COMMENTS set COMMENT_TEXT='New text'
where COMMENT_ID=123
and COMMENT_TEXT='Old Text'
and RATING=5
and ITEM_ID=3
and FROM_USER_ID=45
Кроме того, Hibernate будет включать только изменяемые свойства (только COMMENT_TEXT в данном примере) если вы выставите optimistic-lock=”dirty” (заметим, что этот параметр также требует, чтобы вы установили файл отображения класса в dynamic-update=”true”).
Мы не рекомендуем этот подход; он медленный, более сложный и менее надежный, чем номер версии и не работает, если ваша транзакция приложения охватывает несколько сессий (как в случае, если вы используете несвязанные объекты).
Мы теперь снова переключимся и рассмотрим новый аспект Hibernate. Мы уже упоминали о тесной взаимосвязи между транзакциями и кэшированием во введении этой главы. Основы транзакций и блокировки, а также детализация сессий, имеют важнейшее значение, если учесть кэширование данных на уровне приложения.
О толковых вещах пишешь.. Респект!
ОтветитьУдалить