Автор оригинала: Vlad Mihalcea.
Все операторы базы данных выполняются в контексте физической транзакции, даже если мы явно не объявляем границы транзакции (НАЧАЛО/ФИКСАЦИЯ/ОТКАТ). Целостность данных обеспечивается свойствами ACID транзакций базы данных.
Логическая транзакция-это единица работы на уровне приложения, которая может охватывать несколько физических транзакций (база данных). Сохранение соединения с базой данных открытым на протяжении нескольких запросов пользователей, включая время на обдумывание, безусловно, является анти-шаблоном.
Сервер базы данных может содержать ограниченное количество физических подключений, и часто они используются повторно с помощью пула подключений . Хранение ограниченных ресурсов в течение длительного периода времени препятствует масштабируемости. Таким образом, транзакции базы данных должны быть короткими, чтобы гарантировать, что как блокировки базы данных, так и соединения в пуле будут освобождены как можно скорее.
Веб-приложения предполагают разговорный шаблон чтения, изменения и записи. Веб-диалог состоит из нескольких запросов пользователей, все операции логически связаны с одной и той же транзакцией на уровне приложения. Типичный случай использования выглядит так:
- Алиса запрашивает определенный товар для показа
- Продукт извлекается из базы данных и возвращается в браузер
- Алиса запрашивает модификацию продукта
- Продукт должен быть обновлен и сохранен в базе данных
Все эти операции должны быть объединены в единую единицу работы. Поэтому нам нужна транзакция на уровне приложения, которая также совместима с ACID, потому что другие одновременные пользователи могут изменять одни и те же сущности спустя долгое время после освобождения общих блокировок.
В своем предыдущем посте я рассказал об опасностях потерянных обновлений. Свойства ACID транзакции базы данных могут предотвратить это явление только в пределах одной физической транзакции. Для переноса границ транзакций на уровень приложений требуются гарантии ACID на уровне приложений.
Чтобы предотвратить потерю обновлений, мы должны иметь повторяемые чтения на уровне приложений наряду с механизмами управления параллелизмом.
HTTP – это протокол без состояния. Приложения без состояния всегда легче масштабировать, чем приложения с сохранением состояния, но разговоры не могут быть без состояния.
Hibernate предлагает две стратегии для реализации длительных разговоров :
- Расширенный контекст сохранения
- Отдельные объекты
Расширенный контекст сохранения
После завершения первой транзакции базы данных соединение JDBC закрывается (обычно возвращается в пул соединений), и сеанс гибернации отключается. Новый запрос пользователя приведет к повторному подключению исходного сеанса. Только последняя физическая транзакция должна выполнять операции DML, так как в противном случае транзакция уровня приложения не является атомной единицей работы.
Для отключения сохранения в ходе транзакции на уровне приложения у нас есть следующие опции:
- Мы можем отключить автоматическую очистку , переключив режим очистки сеанса в РУЧНОЙ . В конце последней физической транзакции нам нужно явно вызвать Сеанс#flush() для распространения переходов состояния сущности .
Все транзакции, кроме последней, помечены только для чтения . Для транзакций только для чтения Hibernate отключает как проверку на наличие ошибок, так и автоматическую очистку по умолчанию.
Флаг только для чтения может распространяться на базовое соединение JDBC , поэтому драйвер может включить некоторые оптимизации только для чтения на уровне базы данных.
Последняя транзакция должна быть доступна для записи, чтобы все изменения были сброшены и зафиксированы.
Использование расширенного контекста сохранения более удобно, поскольку сущности остаются подключенными по нескольким пользовательским запросам. Недостатком является объем памяти. Контекст сохранения может легко увеличиваться с каждым новым извлеченным объектом. Механизм проверки на “грязный” режим гибернации по умолчанию использует стратегию глубокого сравнения , сравнивая все свойства всех управляемых объектов. Чем больше контекст сохранения, тем медленнее будет работать механизм грязной проверки.
Это можно смягчить, удалив объекты, которые не нужно распространять на последнюю физическую транзакцию.
Java Enterprise Edition предлагает очень удобную модель программирования за счет использования @с сохранением состояния сеансовых компонентов вместе с РАСШИРЕННЫМ контекстом сохранения .
Во всех примерах контекста расширенной сохраняемости для распространения транзакций по умолчанию устанавливается значение NOT_SUPPORTED , что делает неопределенным, регистрируются ли запросы в контексте локальной транзакции или каждый запрос выполняется в отдельной транзакции базы данных.
Отдельные объекты
Другой вариант-привязать контекст сохранения к жизненному циклу промежуточной физической транзакции. При закрытии контекста сохранения все сущности отсоединяются. Для того чтобы обособленная организация стала управляемой, у нас есть два варианта:
Сущность может быть повторно подключена с помощью метода Hibernate Session.update () . Если уже есть подключенная сущность (тот же класс сущностей и с тем же идентификатором), Hibernate создает исключение, поскольку сеанс может содержать не более одной ссылки на любую данную сущность.
Такого эквивалента в Java Persistence API нет.
- Отдельные объекты также могут быть объединены с их эквивалентом постоянного объекта. Если в данный момент не загружен объект сохранения, Hibernate загрузит его из базы данных. Отделенная сущность не будет управляться.
К настоящему времени вы должны знать, что этот шаблон пахнет неприятностями: Что делать, если загруженные данные не совпадают с тем, что мы загрузили ранее? Что, если сущность изменилась с тех пор, как мы ее впервые загрузили?
Перезапись новых данных более старым снимком приводит к потере обновлений. Таким образом, механизм управления параллелизмом не подходит при работе с длинными разговорами.
Хранилище обособленных объектов
Обособленные сущности должны быть доступны в течение всего срока действия данного длительного разговора. Для этого нам нужен контекст с отслеживанием состояния, чтобы убедиться, что все запросы на разговор находят одни и те же отдельные объекты. Поэтому мы можем использовать:
- Компоненты сеанса с отслеживанием состояния
Компоненты сеанса с сохранением состояния-одна из самых больших функций, предлагаемых Java Enterprise Edition. Он скрывает всю сложность сохранения/загрузки состояния между различными запросами пользователей. Будучи встроенной функцией, она автоматически извлекает выгоду из репликации кластера, поэтому разработчик может вместо этого сосредоточиться на бизнес-логике.
Seam – это платформа приложений Java EE, которая имеет встроенную поддержку веб-разговоров.
- HttpSession
Мы можем сохранить отсоединенные объекты в сеансе HttpSession. Большинство веб-серверов/серверов приложений предлагают репликацию сеансов, поэтому эта опция может использоваться технологиями, не относящимися к JEE, такими как Spring framework . Как только разговор закончится, мы всегда должны отбросить все связанные состояния, чтобы убедиться, что мы не раздули сеанс ненужным хранилищем.
Вам нужно быть осторожным, чтобы синхронизировать весь доступ к HttpSession (getAttribute/setAttribute), потому что по очень странной причине это веб-хранилище не является потокобезопасным .
Spring Web Flow – это компаньон Spring MVC, который поддерживает веб-беседы HttpSession.
- Хейзелкаст
Hazelcast-это кластерный кэш в памяти, поэтому он является жизнеспособным решением для длительного хранения разговоров. Мы всегда должны устанавливать политику истечения срока действия, потому что в веб-приложении разговоры могут быть запущены и прекращены. Истечение срока действия действует как аннулирование сеанса Http.
Как и в случае с транзакциями базы данных, нам нужны повторяющиеся чтения, так как в противном случае мы могли бы загрузить уже измененную запись, не осознавая этого, поэтому:
- Алиса запрашивает товар для показа
- Продукт извлекается из базы данных и возвращается в браузер
- Алиса запросила модификацию продукта
- Поскольку Алиса не сохранила копию ранее отображенного объекта, она должна перезагрузить его еще раз
- Продукт обновляется и сохраняется в базе данных
- Обновление пакетного задания было потеряно, и Алиса никогда этого не поймет
Сохранение состояния разговора необходимо, если мы хотим обеспечить как изоляцию, так и согласованность, но мы все равно можем столкнуться с ситуациями потери обновлений:
Даже если у нас есть повторяемые чтения на уровне приложений, другие все равно могут изменять одни и те же объекты. В контексте одной транзакции базы данных блокировки на уровне строк могут блокировать одновременные изменения, но это невозможно для логических транзакций. Единственный вариант-разрешить другим пользователям изменять любые строки, предотвращая сохранение устаревших данных.
Оптимистическая блокировка-это универсальный метод управления параллелизмом, который работает как для физических транзакций, так и для транзакций на уровне приложений. Использование JPA-это всего лишь вопрос добавления поля @Version в наши доменные модели:
Поскольку это очень интересная тема, я решил также записать видео. Наслаждайтесь просмотром!
Для переноса границ транзакций базы данных на уровень приложений требуется управление параллелизмом на уровне приложений. Чтобы обеспечить повторяемое чтение на уровне приложения, нам необходимо сохранять состояние по нескольким пользовательским запросам, но в отсутствие блокировки базы данных нам необходимо полагаться на управление параллелизмом на уровне приложения.
Оптимистическая блокировка работает как для транзакций на уровне базы данных, так и для транзакций на уровне приложений, и она не использует никакой дополнительной блокировки базы данных. Оптимистическая блокировка может предотвратить потерю обновлений, и именно поэтому я всегда рекомендую, чтобы все объекты были помечены атрибутом @Version.