Реализация блокирования
СУБД использует блокировки, чтобы в каждый момент времени те или иные данные могли изменяться только одной транзакцией. Говоря проще, блокировки — это механизм обеспечения одновременного доступа. При отсутствии определенной модели блокирования, предотвращающей одновременное изменение, например, одной строки, многопользовательский доступ к базе данных попросту невозможен. Однако при избыточном или неправильном блокировании одновременный доступ тоже может оказаться невозможным. Если пользователь или сама СУБД блокирует данные без необходимости, то работать одновременно сможет меньшее количество пользователей. Поэтому понимание назначения блокирования и способов его реализации в используемой СУБД принципиально важно для создания корректных и масштабируемых приложений.
Принципиально важно также понимать, что в различных СУБД блокирование реализовано по-своему. В одних — используется блокирование на уровне страниц, в других — на уровне строк; в некоторых реализациях выполняется эскалация блокировок со строчного на страничный уровень, в других — не выполняется; в некоторых СУБД используются блокировки чтения, в других — нет; в одних СУБД реализуется уровень изолированности транзакций SERIALIZABLE с помощью блокирования, а в других — через согласованные по чтению представления данных (без установки блокировок). Эти небольшие отличия могут перерасти в огромные проблемы, связанные с производительностью, или даже привести к возникновению ошибок в приложениях, если не понимать их особенностей.
Ниже приведены принципы блокирования в СУБД Oracle.
Oracle блокирует данные на уровне строк и только при изменении. Эскалация блокировок до уровня блока или таблицы никогда не выполняется.
Oracle никогда не блокирует данные с целью считывания. При обычном чтении блокировки на строки не устанавливаются.
Сеанс, записывающий данные, не блокирует сеансы, читающие данные. Повторю: операции чтения не блокируются операциями записи. Это принципиально отличается от практически всех остальных СУБД, в которых операции чтения блокируются операциями записи.
Сеанс записи данных блокируется, только если другой сеанс записи уже заблокировал строку, которую предполагается изменять. Сеанс считывания данных никогда не блокирует сеанс записи.
Эти факты необходимо учитывать при разработке приложений, однако следует помнить, что эти принципы используются только в Oracle. Разработчик, не понимающий, как используемая СУБД обеспечивает одновременный доступ, неизбежно столкнется с проблемами целостности данных (особенно часто это происходит, когда разработчик переходит с другой СУБД на Oracle, или наоборот, и не учитывает в приложении различия механизмов обеспечения одновременного доступа).
Один из побочных эффектов принятого в СУБД Oracle "неблокирующего" подхода состоит в том, что если действительно необходимо обеспечить доступ к строке не более чем одного пользователя в каждый момент времени, то именно разработчику необходимо предпринять для этого определенные усилия. Рассмотрим следующий пример. Один разработчик показывал мне только что завершенную им программу планирования ресурсов (учебных классов, проекторов и т.д.), находящуюся в стадии внедрения. Это приложение реализовало бизнес-правило, предотвращающее выделение ресурса более чем одному лицу на любой период времени. То есть, приложение содержало специальный код, который проверял, что никто из пользователей не затребовал ресурс на тот же период времени (по крайней мере разработчик думал, что его код это проверяет). Код обращался к таблице планов и, если в ней не было строк с перекрывающимся временным интервалом, вставлял в нее новую строку. Итак, разработчик просто работал с парой таблиц:
create table resources ( resource_name varchar2(25) primary key, ... ); create table schedules ( resource_name varchar2(25) references resources, start_time date, end_time date );
И прежде чем зарезервировать на определенный период, скажем, учебный класс, приложение выполняло запрос вида:
select count(*) from schedules where resource_name = :room_name and (start_time between :new_start_time and :new_end_time or end_time between :new_start_time and :new_end_time)
Он казался разработчику простым и надежным: если возвращено значение 0, учебный класс можно занимать; если возвращено ненулевое значение, значит, учебный класс на этот период уже кем-то занят. Ознакомившись с используемым алгоритмом, я подготовил простой тест, показывающий, какая ошибка будет возникать при реальной эксплуатации приложения. Эту ошибку будет крайне сложно обнаружить и тем более установить ее причину, — кому-то может даже показаться, что это ошибка СУБД.
Я предложил его коллеге сесть за соседний компьютер, перейти на тот же экран и попросил на счет три обоих нажать на кнопку
Go и попытаться зарезервировать класс на одно то же время. Оба смогли это сделать — то, что прекрасно работало в изолированной среде, не сработало в среде многопользовательской. Проблема в этом случае была вызвана неблокирующим чтением в Oracle. Ни один из сеансов не блокировал другой. Оба сеанса просто выполняли представленный выше запрос и применяли алгоритм резервирования ресурса. Они оба могли выполнять запрос, проверяющий занятость ресурса, даже если другой сеанс уже начал изменять таблицу планов (это изменение невидимо для других сеансов до фиксации, то есть до тех пор, когда уже слишком поздно). Поскольку сеансы никогда не пытались изменить одну и ту же строку в таблице планов, они никогда и не блокировали друг друга, вследствие чего бизнес-правило не срабатывало так, как ожидалось.
Разработчику необходим метод реализации данного бизнес-правила в многопользовательской среде, способ, гарантирующий что в каждый момент времени только один сеанс резервирует данный ресурс. В данном случае решение состояло в программном упорядочении доступа — кроме представленного выше запроса
count(*), необходимо было сначала выполнить:
select * from resources where resource_name = :room_name FOR UPDATE;
Ранее в этой главе рассматривался пример, когда использование конструкции
FOR UPDATE приводило к проблемам, но в этом случае она обеспечивает корректную работу бизнес-правила. Мы просто блокируем ресурс (учебный класс), использование которого планируется, непосредственно перед его резервированием, до выполнения запроса к таблице планов, выбирающего строки для данного ресурса. Блокируя ресурс, который мы пытаемся зарезервировать, мы гарантируем, что никакой другой сеанс в это же время не изменяет план использования ресурса. Ему придется ждать, пока наша транзакция не будет зафиксирована — после этого он сможет увидеть сделанное в ней резервирование. Возможность перекрытия планов, таким образом, устранена. Разработчик должен понимать, что в многопользовательской среде иногда необходимо использовать те же приемы, что и при многопотоковом программировании. В данном случае конструкция
FOR UPDATE работает как семафор. Она обеспечивает последовательный доступ к конкретной строке в таблице ресурсов, гарантируя, что два сеанса одновременно не резервируют ресурс.
Этот подход обеспечивает высокую степень параллелизма, поскольку резервируемых ресурсов могут быть тысячи, а мы всего лишь гарантируем, что сеансы изменяют конкретный ресурс поочередно. Это один из немногих случаев, когда необходимо блокирование вручную данных, которые не должны изменяться. Требуется уметь распознавать ситуации, когда это необходимо, и, что не менее важно, когда этого делать не нужно (пример, когда не нужно, приведен далее). Кроме того, такой прием не блокирует чтение ресурса другими сеансами, как это могло бы произойти в других СУБД, благодаря чему обеспечивается высокая масштабируемость.
Подобные проблемы приводят к масштабным последствиям при переносе приложения с одной СУБД на другую (к этой теме я вернусь чуть позже), которых разработчикам сложно избежать. Например, при наличии опыта разработки для другой СУБД, в которой пишущие сеансы блокируют читающих, и наоборот, разработчик может полагаться на это блокирование как защищающее от подобного рода проблем — именно так все и работает во многих СУБД, отличных от Oracle. В Oracle приоритет отдан одновременности доступа, и необходимо учитывать, что в результате все может работать по-другому.
В 99 процентах случаев блокирование выполняется незаметно, и о нем можно не заботиться. Но оставшийся 1 процент надо научиться распознавать. Для решения этой проблемы нет простого списка критериев типа "в таком-то случае надо сделать то-то". Нужно понимать, как приложение будет работать в многопользовательской среде и что оно будет делать в базе данных.
Содержание раздела