Предотвращение потери данных в базе Oracle

Илья Дергунов

Илья Дергунов

Автор статьи. ИТ-специалист с 20 летним стажем, автор большого количества публикаций на профильную тематику (разработка ПО, администрирование, новостные заметки). Подробнее.

Как предотвратить потерю данных в базе OracleОдин из побочных эффектов применения неблокирующего подхода Oracle состоит в том, что в действительности необходимо обеспечить, чтобы во время выполнения каких-либо действий самим разработчиком доступ к строке предоставлялся не более чем одному пользователю.

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

EODA@ORA12CR1> create table resources
( resource_narne varchar2(25) primary key,
other_data	varchar2(25)
) ;
Таblе created.
Таблица создана.
EODA@ORA12CR1> create tаblе schedules
( resource_narne varchar2(25) references resources,
start_tirne date,
end_time	date
) ;
Таble created.

Таблица создана.

Сразу после вставки записи о бронировании помещения в таблицу SCHEDULES, но перед фиксацией приложение выполняло запрос:

EODA@ORA12CR1> select count(*)
from schedules
where resource_narne = : resource_narne
and (start_tirne < :new_end_tirne)
AND (end_tirne > :new_start_tirne)
/

Все казалось простым и “пуленепробиваемым” (во всяком случае, разработчику); если результатом подсчета был 0, то помещение поступало в ваше распоряжение. Если результат подсчета больше 0, то помещение не может быть зарезервировано на заявленный период времени. Выяснив, в чем состояла логика разработчика, я выполнил очень простой тест, чтобы показать ему ошибку, которая должна была проявиться в процессе эксплуатации приложения — ошибку, которую было бы невероятно трудно отследить и диагностировать. Многие были бы убеждены, что это должна быть программная ошибка базы данных.

Все что я сделал — это попросил кого-то воспользоваться соседним терминалом. Оба пользователя (разработчик и второе лицо) открыли один и тот же экран приложения, и “на счет три” каждый из них щелкнул на кнопке выполнения и попытался зарезервировать одно и то же помещение на почти перекрывающийся период времени. Обоим пользователям удалось выполнить резервирование. Логика, которая прекрасно работала в условиях изоляции, дала сбой в многопользовательской среде. В этом случае проблема частично была вызвана неблокирующими операциями чтения Oracle. Один из сеансов никак не блокировал другой. Оба сеанса просто запустили запрос и выполнили действия по резервированию помещения. Оба сеанса смогли вьщать запрос для просмотра информации о резервировании, даже если один из них уже начал модифицировать таблицу SCHEDULES (изменение не будет видно другому сеансу вплоть до выполнения операции фиксации, ко времени которой уже слишком поздно). Поскольку казалось, что пользователи никогда не будут пытаться изменять одну и ту же строку в таблице SCHEDULES, то их блокировка не производилась и, следовательно, бизнес-правило не могло вступить в силу, как было задумано.

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

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

select * from resources where resource_name = :resource_name FOR UPDATE;

Здесь производится блокирование ресурса (помещения), предназначенного для резервирования, непосредственно перед его резервированием — иначе говоря, до запроса этого ресурса из таблицы SCHEDULES. За счет блокирования ресурса, который пользователь пытается зарезервировать, разработчик гарантирует, что никто другой в то же самое время не сможет изменить график эксплуатации этого ресурса. Любым пользователям, желающим выполнить оператор SELECT FOR UPDATE для данного ресурса, придется ожидать, пока не будет осуществлена фиксация транзакции, после чего они получат возможность увидеть график использования ресурса. Шансы перекрытия графиков исчезали. Разработчики обязаны понимать, что в многопользовательской среде они должны иногда применять приемы, аналогичные используемым при многопоточном программировании. В рассматриваемом случае конструкция FOR UPDATE действует подобно семафору. Она обеспечивает последовательный доступ к конкретной строке таблицы RESOURCES, не позволяя каким-то двум пользователям зарезервировать ресурс одновременно.

Применение подхода FOR UPDATE по-прежнему обеспечивает высокую степень параллелизма, т.к. потенциально могут резервироваться тысячи ресурсов. Это тот редкий случай, когда устанавливается ручная блокировка данных, которые в действительности мы обновлять не собираемся. Вы должны уметь распознавать ситуации, когда необходимо блокировать вручную, и что вероятно еще важнее - когда этого делать не следует (позже будет приведен соответствующий пример). Более того, конструкция FOR UPDATE не блокирует чтение данных о ресурсе другими пользователями, как это бывает в других базах данных. В результате такой подход будет очень хорошо масштабироваться.

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

Мне приходилось сталкиваться с ситуациями, когда разработчики, даже ознакомившись с подобным примером, поднимали на смех идею о том, что им необходимо понимать, каким образом все это работает. Они говорили: “Мы просто помечаем флажок ‘транзакционное’ нашего приложения Hibernate и оно само заботится обо всем, что связано с транзакциями; нам незачем знать всю эту кухню”. Я спрашивал их: “Так что, Hibernate будет генерировать отличающийся код для SQL Server, DB2 и Oracle — совершенно разный код, разное количество операторов SQL, разную логику?”. Они отвечали — нет, но приложение будет транзакционным. Возникала путаница. Транзакционное в данном контексте означает просто поддержку фиксации и отката, а не то, что код транзакционно согласован (читай: что код корректен). Независимо от инструмента или платформы, используемой для доступа к базе данных, знание средств управления параллельным доступом жизненно важно, если вы не хотите повредить свои данные.

На протяжении 99% времени блокировка совершенно прозрачна, и о ней можно не беспокоиться. Но именно оставшийся один процент случаев нужно уметь распознавать. Для решения этой проблемы не существует какого-то простого контрольного перечня типа “чтобы сделать это, необходимо выполнить то”. Все зависит от понимания поведения приложения в многопользовательской среде и в базе данных.

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

 

Вас заинтересует / Intresting for you:

Oracle IDE: JDeveloper, SQL De...
Oracle IDE: JDeveloper, SQL De... 4522 просмотров Ольга Потемкина Tue, 21 Nov 2017, 13:18:46
Oracle и Java: использование P...
Oracle и Java: использование P... 5750 просмотров sepia Tue, 08 May 2018, 08:52:34
Oracle Text и PL/SQL: многоязы...
Oracle Text и PL/SQL: многоязы... 3477 просмотров Максим Николенко Fri, 18 Oct 2019, 12:41:03
Apex Oracle обучающий видеокур...
Apex Oracle обучающий видеокур... 8805 просмотров Дэн Sun, 05 Aug 2018, 16:36:33
Войдите чтобы комментировать