Как не потерять данные при работе веб приложения?

Введение

В этой статье я расскажу как не потерять данные при работе веб-приложения.

Все запросы БД выполняются в контексте физической транзакции, даже если мы не делаем явного объявления границ транзакций (BEGIN/COMMIT/ROLLBACK). Целостность данных обеспечивается свойствами ACID транзакций БД.

Логические vs Физические транзакции

Логическая транзакция это единица работы на уровне приложения, которая может охватывать множество физических (БД) транзакций. Держать соединение к БД открытым на протяжении выполнении нескольких пользовательских запросов, включая время обдумывания пользователем, является анти-паттерном.

СУБД может предоставить лишь ограниченное число физических соединений и поэтому они должны переиспользоваться, используя пулинг соединений. Удерживание ограниченных ресурсов на длительные периоды времени препятствует масштабируемости. Поэтому транзакции БД не должны быть большими, для обеспечения гарантий того что и блокировки БД и соединения из пула будут освобождены, так быстро насколько это возможно.

Веб приложения используют паттерн «чтение-изменение-запись» для ведения диалогов. Веб диалоги состоят из множественных запросов пользователя, все операции  логически соединены с одной и той же транзакцией уровня приложения. Типичные варианты использования представлены ниже:

  1. User запрашивает определенный продукт для отображения на форме
  2. Продукт извлекается из БД и возвращается браузеру
  3. User запрашивает изменение продукта
  4. Продукт должен быть обновлен и сохранен в БД

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

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

Для предотвращения потерь, мы должны иметь уровень изоляции Repeatable Read вместе с механизмами контроля за параллельным выполнением.

Длительные диалоги

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

Hibernate предлагает две стратегии для имплементирования долгих диалогов:

  • Расширенный persistence контекст
  • Detached объекты

Расширенный persistence контекст

После того как первая транзакция БД выполнится, JDBC соединение закрывается (обычно возвращается в пул соединений) и сессия Hibernate становится отсоединенной. Новый запрос пользователя заново присоединит оригинальную сессию. Только последняя физическая операция должна выполнить DML операции в БД, иначе логическая транзакция не является атомарной единицей работы.

Для выключения режима «делать flush после каждой операции» на протяжении выполнения логической транзакции у нас есть несколько опций:

  • Мы можем выключить автоматический flushing, переключив Session FlushMode в режим MANUAL. В конце последней физической транзакции, мы должны явно вызвать Session#flush() для синхронизации изменений состояний сущностей с записями  БД.
  • Все кроме последней транзакции помечены read-only. Для read-only транзакций Hibernate выключает «грязную» проверку и автоматический flushing по умолчанию.

Флаг «read-only» может быть распространен на нижележащее JDBC соединение, так что драйвер может включить некоторую оптимизацию на уровне БД.

Последняя транзакция должны быть доступна для записи, так что все изменения будут сброшены в БД (flushed) и закоммичены.

Использование расширенного persistence контекста более удобно потому что сущности остаются присоединенными на протяжении множественных пользовательских запросов. Недостаток — это объем необходимой памяти. Persistence контекст может легко вырасти с каждой извлеченной сущностью. Дефолтовый механизм «грязной» проверки Hibernate (dirty checking) использует стратегию «глубокого сравнения», сравнивая все свойства всех контролируемых сущностей. Чем больше persistence контекст, тем медленнее происходит механизм «грязной» проверки.

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

Java EE предлагает очень удобную программную модель с помощью использования @Stateful Session бинов вместе с EXTENDED PersistenceContext.

Detached объекты (Отсоединенные объекты)

Другой опцией является привязка persistence контекста к жизненному циклу физической транзакции. После того как контекст закроется все сущности становятся detached (отсоединенными). Для того, чтобы отсоединенная сущность стала присоединенной, у нас есть два пути:

  • Сущность может быть повторно подключена к сессии с помощью специального метода Hibernate Session.update().  Если уже есть аналогичная присоединенная сущность (такой же класс и с одинаковым идентификатором) Hibernate бросит исключение, потому что Сессия не может иметь более чем одну ссылку на любую имеющуюся сущность.

В Java Persistence API пока нет аналогичного подхода.

  • Отсоединенная сущность может также объединена с ее эквивалентом, содержащимся в persistence контексте. Если нет уже загруженных объектов, то Hibernate загрузит его из БД. Отсоединенная сущность не станет управляемой.

Но нужно знать, что такой способ таит в себе несколько неоднозначностей:

Что если загруженный объект не соответсвует тому, который мы ранее загрузили?

Что если сущность была изменена с тех пор как мы ее первый раз загрузили?

Перезапись новыми данными старого экземпляра объекта приводит к потерям обновлений. Так что механизм контроля над параллельным выполнением не является хорошим способом при долгих диалогах в приложении.

И Hibernate и  JPA предлагают объединение сущностей.

Хранилище detached сущностей

Отсоединенные сущности должны быть доступными на время жизненного цикла данного продолжительного веб диалога. Для этого нам нужен контекст с хранением состояния для того чтобы быть уверенным что все реквесты в процессе ведения диалогов обрабатывают те же самые detached сущности. Таким образом, мы можем использовать:

Stateful session бины одно из самых лучших решений предлагаемых Java EE. Он скрывает всю сложность сохранения/загрузки состояния между запросами разных пользователей. Будучи встроенной фичей, он автоматически приносит пользу от кластерной репликации, так что разработчики могут сконцентрироваться на бизнес логике.

Seam — Java EE фреймворк, который имеет встроенную поддержку веб диалогов.

Мы можем хранить отсоединенные объекты в Http сессии. Большинство web/application серверов предоставляют репликацию сессий так что данная опция может быть использована другими не Java EE технологиями, как Spring Framework. Как только диалог закончен, мы должны всегда сбрасывать все связанные состояния, для того чтобы убедиться что мы не увеличили размер сессии до неприличных размеров.

Нужно использовать осторожно методы доступа к HttpSession (getAttribute/setAttribute), потому что это веб хранилище не является потокобезопасным.

Hazelcast — in-memory кластерный кэш и видится вполне пригодным решением для хранения продолжительных диалогов. Мы всегда должны устанавливать срок жизни кэша, потому что в веб приложениях диалоги могут быть начаты и заброшены. По истечению срока происходит очистка Http сессии.

Анти-паттерн диалога без хранения состояний

Как и транзакциями БД, нам нужны повторные чтения, иначе мы можем загрузить измененную запись, не осознавая этого:

 

conversationlostupdatebyreloading

  1. Пользователь Alice запрашивает продукт для отображения
  2. Продукт извлекается из БД и возвращается в браузер
  3. Alice запрашивает модификацию продукта
  4. Потому что не сохранила копию предыдущего отображенного объекта, она должна загрузить снова один раз
  5. Продукт обновился и сохранился в БД
  6. Обновления из батча были потеряны и Alice никогда их не получит

Анти-паттерн ведения диалогов с хранением состояния без версии

Мы должны сохранять состояния в процессе ведения диалога, если хотим гарантировать и изоляцию и консистентность, но мы все еще не убереглись от ситуаций с  потерями обновлений:

conversationlostupdatestatefullunversioned

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

Оптимистическая блокировка спасет мир

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

conversationlostupdatestatefullversioned

Заключение

Для поднятия границы транзакций БД до уровня приложения требуется установить контроль за параллельным исполнением транзакций также и на уровне приложения. Для обеспечения повторных операций чтения нам нужно хранить состояния между множественными запросами пользователей.

Оптимистические блокировки работают как для транзакций уровня БД, так и для транзакций уровня приложения и дают возможность не использовать любые дополнительные блокировки на уровне БД. Оптимистическое блокирование может предотвратить потери обновлений и именно поэтому я всегда рекомендую аннотировать все сущности @Version.

(Visited 617 times, 4 visits today)

Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.