БЛОКИРОВКИ ЗАПИСЕЙ
Представьте себе такую ситуацию, два пользователя одновременно получили доступ к одной и той же записи и начинают ее редактировать. Результат таких действий непременно станет неприятным сюрпризом как для одного, так и для другого пользователя. Для недопущения подобных ситуаций в OpenEdge ABL применяются блокировки записей. Всего таких блокировок, или как их еще называют - локировок (от англ. lock) существует 2 типа:
- SHARE-LOCK - запись блокирована для изменения, но доступна для чтения;
- EXCLUSIVE-LOCK - запись заблокирована как для изменения, так и для чтения.
Каждой читаемой из таблицы базы данных записи AVM, по умолчанию, автоматически выставляет блокировку SHARE-LOCK. Это означает, что другие пользователи в этот момент не смогут изменять данную запись, но при этом смогут ее прочитать.
FIND FIRST loan WHERE loan.branch-id EQ "0001". ← блокировка SHARE-LOCK
DISPLAY loan.open-date. ← продолжает действовать SHARE-LOCK
Действие блокировки SHARE-LOCK распространяется до тех пор, пока запись находится в области видимости, т.е. пока она находится в буфере записи.
В момент когда запись переходит в статус редактирования, такой записи AVM автоматически повышает уровень блокировки до EXCLUSIVE-LOCK. В этот момент, данная запись становится недоступной другим пользователям как для изменения, так и для чтения с блокировкой SHARE-LOCK.
FIND FIRST loan WHERE loan.branch-id EQ "0001". ← блокировка SHARE-LOCK
UPDATE loan.open-date. ← блокировка повышается до EXCLUSIVE-LOCK
DISPLAY loan.open-date ← продолжает действовать EXCLUSIVE-LOCK
Действие блокировки EXCLUSIVE-LOCK распространяется до завершения транзакции.
В приведенном примере, транзакционным блоком является вся процедура и поэтому действие блокировки EXCLUSIVE-LOCK будет распространяться до завершения процедуры.
Если по завершению транзакции запись продолжает быть доступной, т.е. остается в буфере записи, то блокировка такой записи автоматически понижается до SHARE-LOCK.
DO TRANSACTION:
FIND FIRST loan WHERE loan.branch-id EQ "0001". ← блокировка SHARE-LOCK
UPDATE loan.open-date. ← блокировка повышается до EXCLUSIVE-LOCK
END. ← блокировка понижается до SHARE-LOCK
DISPLAY loan.open-date ← продолжает действовать SHARE-LOCK
После того, как запись удаляется из буфера записи выставленная ей блокировка снимается. Освободить запись, работа с которой уже завершена, можно с помощью оператора RELEASE.
RELEASE record [NO-ERROR]
record - идентификатор буфера записи.
RELEASE осуществляет сохранение записи в таблицу и удаляет ее из буфера записи.
DO TRANSACTION:
FIND FIRST loan WHERE loan.branch-id EQ "0001". ← блокировка SHARE-LOCK
UPDATE loan.open-date. ← блокировка повышается до EXCLUSIVE-LOCK
END. ← блокировка понижается до SHARE-LOCK
DISPLAY loan.open-date ← продолжает действовать SHARE-LOCK
RELEASE (loan). ← снятие блокировки
Если другой пользователь, в момент редактирования записи первым пользователем, т.е. когда для нее установлена EXCLUSIVE-LOCK блокировка, попытается прочитать ее с SHARE-LOCK или EXCLUSIVE-LOCK блокировкой, ему будет выдано сообщение о том, что запись заблокирована и он будет вынужден либо прервать выполнение своей процедуры, либо ожидать пока первый пользователь завершит редактирование записи и ее блокировка сменится с EXCLUSIVE-LOCK на SHARE-LOCK, или будет полностью разблокирована.
loan in use by user-name on pts/6. Wait or press CTRL-C to stop. (121)
Аналогичная ошибка возникнет при попытке изменить, или прочитать запись с блокировкой EXCLUSIVE-LOCK, если другим пользователем ей уже установлена блокировка SHARE-LOCK.
Время ожидания разблокировки записи не является бесконечным и определяется стартовым параметром сессии -lkwtmo, значение которого по умолчанию составляет 30 минут, а минимально допустимое значение 60 секунд. Если второй пользователь примет решение ожидать когда первый пользователь отпустит заблокированную им запись и по истечению этого времени запись так и не будет освобождена, у второго пользователя возникнет событие STOP.
Использование FIND с опцией NO-WAIT позволит второму пользователю продолжить выполнение его процедуры не дожидаясь освобождения заблокированной записи.
При этом, так как запись не была все же прочитана (не помещена в буфер записи), то и обработана она быть не сможет.
FIND FIRST loan WHERE loan.branch-id EQ "0001" EXCLUSIVE-LOCK NO-WAIT.
IF AVAIL loan THEN
DO:
...
END.
Для достоверного логирования и информирования пользователя о причине, по которой запись не была обработана, нам потребуется как-то отличать ситуацию, когда запись была заблокирована другим пользователем, от ситуации когда искомая запись не была найдена в таблице.
Для этих целей в ABL применяется функция LOCKED, возвращающая логическое значение TRUE, когда искомая с помощью FIND ... NO-WAIT запись не доступна по причине наличия у нее конфликтующей блокировки установленной другим пользователем.
LOCKED record
где, record - буфер записи используемый в операторе FIND.
FIND FIRST loan WHERE loan.branch-id EQ "0001" EXCLUSIVE-LOCK NO-WAIT.
IF NOT AVAIL loan THEN
DO:
IF LOCKED loan THEN MESSAGE "Запись заблокирована другим пользователем".
ELSE MESSAGE "Запись не найдена".
END.
Еще одним инструментом, позволяющим избегать конфликтных ситуаций, является функция CAN-FIND. Данная функция позволяет определить наличие в таблице искомой записи и возможность установки ей заданной блокировки.
CAN-FIND
(
[FIRST | LAST ] record [constant]
[OF table] [WHERE expression ] [USE-INDEX index]
[USING [FRAME frame] field
[AND [FRAME frame] field] ...
]
[SHARE-LOCK | NO-LOCK] [NO-WAIT] [NO-PREFETCH]
Если нам необходимо только прочитать запись из базы данных и мы не планируем ее изменять, то для исключения конфликтных ситуаций с другими пользователями необходимо осуществлять поиск нужной нам записи с опцией NO-LOCK.
FIND FIRST loan WHERE loan.branch-id EQ "0001" NO-LOCK.
Использование NO-LOCK указывает на то, что никакой блокировки читаемой записи осуществляться не будет, т.е. не будет устанавливаться используемая по умолчанию блокировка SHARE-LOCK и запись останется доступной другим пользователям как для чтения, так и для изменения. Кроме этого, применение опции NO-LOCK позволит нам читать записи имеющие блокировку EXCLUSIVE-LOCK, но при этом есть риск прочитать частично измененные другим пользователем данные.
Никогда не следует читать запись с опцией NO-LOCK, если вы планируете ее изменять.
FIND FIRST loan WHERE loan.branch-id = "0001" NO-LOCK. ← блокировки нет
DO TRANSACTION:
UPDATE loan.open-date. ← блокировка повышается до EXCLUSIVE-LOCK
END. ← блокировка понижается до SHARE-LOCK
← продолжает действовать блокировка SHARE-LOCK
По завершению изменения записи, ее блокировка не будет снята, а будет понижена до SHARE-LOCK.
Далее рассмотрим как происходит расстановка блокировок записей в итерационных блоках, на примере FOR EACH.
FOR EACH loan WHERE loan.branch-id EQ "0001": ← блокировка SHARE-LOCK
DISPLAY loan.open-date. ← продолжает действовать блокировка SHARE-LOCK
END. ← снятие блокировки
Блокировка записи устанавливается в начале итерации блока, и снимается по ее завершению.
FOR EACH loan WHERE loan.branch-id EQ "0001": ← блокировка SHARE-LOCK
UPDATE loan.open-date. ← блокировка повышается до EXCLUSIVE-LOCK
END. ← снятие блокировки
Каждая итерация блока FOR в приведенном примере, является транзакцией, по завершению которой запись удаляется из буфера записей, в следствии чего, по завершению итерации блока блокировка записи снимается, а не понижается до SHARE-LOCK.
Все блокировки, устанавливаемые внутри транзакционнго блока, сохраняются до его завершения.
DO TRANSACTION:
FOR EACH loan WHERE loan.branch-id EQ "0001": ← блокировка SHARE-LOCK
UPDATE loan.open-date. ← блокировка повышается до EXCLUSIVE-LOCK
END.
← все записи сохраняют блокировку EXCLUSIVE-LOCK
END. ← снятие всех блокировок
Аналогично оператору FIND, оператор FOR так же может использоваться с опцией NO-LOCK.
FOR EACH loan WHERE loan.branch-id EQ "0001" NO-LOCK:
DISPLAY loan.open-date.
END.
Рассмотрим еще один интересный пример. Два пользователя, одновременно, запускают выборки по таблице договоров (loan). Первый пользователь отбирает договора у которых loan.branch-id = "0001" с блокировкой EXCLUSIVE-LOCK, а второй пользователь отбирает договора у которых loan.branch-id = "0100" с блокировкой SHARE-LOCK.
процедура 1-го пользователя:
FOR EACH loan WHERE loan.branch-id = "0001" EXCLUSIVE-LOCK:
DISPL loan.cont-code.
END.
процедура 2-го пользователя:
FOR EACH loan WHERE loan.branch-id = "0100":
DISPL loan.cont-code.
END.
Если вы проведете такой эксперимент, то заметите, что эти два пользователя совершенно не мешают друг другу. Это и правильно, ведь их выборки не пересекаются.
Немного изменим процедуру 1-го пользователя и повторим эксперимент:
FOR EACH loan WHERE CAN-DO("000*",loan.branch-id) EXCLUSIVE-LOCK:
DISPL loan.cont-code.
END.
Теперь они стали мешаться друг другу, хотя запросы казалось бы по-прежнему не пересекаются. Причина такого поведения, как вы наверно смогли уже догадаться, кроется в использовании функции CAN-DO в условиях выборки. Почему так происходит?
Для ответа на данный вопрос нам необходимо вспомнить, что при реализации запроса используется брэкетирование и что прочитанных из базы данных записей может оказаться много больше чем тех, что будут обрабатываться в теле блока FOR. Указанная в запросе блокировка выставляется для каждой читаемой из базы данных записи, и в простом случае, как только выясняется что запись не удовлетворяет полному условию запроса - она тут же освобождается.
Таким образом, запросы, формируемые брэкеты которых пересекаются, в действительности могут мешать друг другу, если одновременно попытаются прочитать одну и туже запись с конфликтующими блокировками.
Аналогичная ситуация могла бы возникнуть если бы первый пользователь вместо оператора FOR осуществлял бы поиск записи с помощью оператора FIND FIRST.
FIND FIRST loan WHERE CAN-DO("000*",loan.branch-id) EXCLUSIVE-LOCK:
IF AVAIL loan THEN
DISPL loan.cont-code.
При выполнении данной процедуры заблокированными окажутся все записи, который будут прочитаны из таблицы loan до того, пока не будет найдена искомая запись.
Описанная в самом начале статьи ситуация, когда два пользователя пытаются отредактировать одну и ту же запись все же возможна. Возникнуть она может тогда, когда два пользователя уже прочитали одну и ту же запись с блокировкой SHARE-LOCK и оба пытаются отредактировать ее, т.е. повысить уровень ее блокировки до EXCLUSIVE-LOCK. Никто из них не сможет этого сделать, так как каждому из них будет мешать выставленная другим пользователем блокировка SHARE-LOCK, и каждый из них получит сообщение о том, что запись уже заблокирована другим пользователем.
Все выставляемые SHARE-LOCK и EXCLUSIVE-LOCK блокировки учитываются в системной таблице _lock. Запись в этой таблице автоматически формируется для каждой записи, которой выставлена SHARE-LOCK или EXCLUSIVE-LOCK блокировка. Размер таблицы по умолчанию составляет 8192 записи и может быть изменен с помощью стартового параметра -L n (Lock Table Entries). Минимально допустимый размер таблицы составляет 32 записи, а максимальный ограничивается доступной памятью системы. При этом, выставляемый размер таблицы должен быть кратным 32. Если указанное значение не является таковым, то OpenEdge автоматически изменит его на ближайшее большее к нему значение кратное 32.
Переполнение системной таблицы блокировок приведет к возникновению системной ошибки:
SYSTEM ERROR: Record lock table too small. Increase -L parameter.
Растановка блокировок записей в OpenEdge осуществляется исключительно в многопользовательском режиме. Если вам, например, необходимо произвести изменения всех записей таблицы базы данных в единой транзакции, то такую процедуру следует осуществлять в однопользовательском режиме.
Подведем итоги всего сказанного выше. Если вам необходимо только прочитать записи и никаких их изменений вы не планируете, то в данном случае поиск и выборка записей должны осуществляться с опцией NO-LOCK. В случае, когда необходимо прочитать запись и быть уверенным в том, что прочитанные данные не будут изменены до завершения процедуры или блока, то стоит применять блокировку SHARE-LOCK, отставляя тем самым другим пользователям возможность читать эти же записи. Если же вы отбираете записи с целью их модификации, то поиск и выборку таких записей следуют осуществлять с опцией EXCLUSIVE-LOCK.
FIND FIRST loan WHERE loan.branch-id EQ "0001". ← блокировка SHARE-LOCK