Блокирование записей
Операторы манипулирования данными.
Диаграмма (Рис. 8) показывает операторы манипулирования данными в ABL и их соответствие четырем уровням взаимодействия:
- Запись в базе данных или временной таблице (Database Record).
- Запись в буфере записи (Record Buffer).
- Запись в буфере экрана, как она видна пользователю (Screen Buffer).
- Действия пользователя над буфером экрана (User).

Из диаграммы видно, что некоторые операторы охватывают несколько уровней. Это делает такие операторы очень экономичными в смысле синтаксиса и объема кода в хост-терминал и клиент-сервер системах. Но они же становятся проблемой в распределенных системах. Это относится к таким операторам, как INSERT, SET, UPDATE, OPEN QUERY (связанный с BROWSE).
За каждым таким оператором неявно (или явно) следует оператор RELEASE, записывающий измененную запись в базу данных. Другими словами, эти операторы обеспечивают полный путь данных от пользователя до базы данных, что невозможно осуществить в распределенных системах.
Виды блокирования записей в Progress
Когда Вы читаете запись из таблицы базы данных, Progress устанавливает определяемый программистом уровень блокирования записи, так что Вы можете предотвратить конфликты при чтении и изменении данных в многопользовательской среде. Блокирование не применяется к записям временных таблиц, так как они локальны для сессии.
По умолчанию операторы FIND, FOR EACH, или GET по query, не связанному с browse, блокируют записи с уровнем SHARE-LOCK. Другой пользователь может также читать эту же запись.
Вы также можете явно указать желаемый уровень блокирования в операторах FIND, FOR EACH и OPEN QUERY. Если вы читаете запись с намерением ее изменить (или удалить), вы можете указать EXCLUSIVE-LOCK. Это помечает запись как зарезервированную исключительно для Вашей сессии. Если какой-либо другой пользователь уже установил SHARE-LOCK на эту запись, попытка прочитать ее с EXCLUSIVE-LOCK не удается. И наоборот, запись, заблокированную с EXCLUSIVE-LOCK, нельзя прочитать с SHARE-LOCK.
Если Вы не намерены изменять записи, и хотите просто читать записи независимо от их блокирования другими пользователями, вы можете использовать опцию NO-LOCK. Следует всегда помнить, что при этом вы можете прочитать часть незавершенной транзакции. Режим NO-LOCK по умолчанию устанавливается для QUERY, связанного с BROWSE.
Апгрейд блокировки.
Если запись прочитана с блокировкой SHARE-LOCK и выполняется попытка изменить данные в записи (любая операция, изменяющая Record Buffer из Screen Buffer, или прямое присваивание полю), Progress пытается повысить (upgrade) уровень блокировки до EXCLUSIVE-LOCK. Если другой пользователь также прочитал эту же запись с SHARE-LOCK, повысить уровень блокировки не получится. Если теперь и второй пользователь попытается изменить запись, возникнет клинч (dead lock, deadly embrace), когда оба пользователя будут ждать освобождения одной и той же записи.
Во избежание таких конфликтов следует всегда явно запрашивать блокирование EXCLUSIVE-LOCK, если Вы намерены изменять запись.
Если клинч все же возник, система освободит заблокированную запись по истечении тайм-аута (по умолчанию – 30 минут).
Замечание: если запись прочитана с режимом NO-LOCK, апгрейд блокировки невозможен – для изменения записи Вы должны прочитать запись снова с опцией EXCLUSIVE-LOCK.
Обработка конфликтов блокировки.
Progress позволяет обработать конфликт блокировки, если Вы не хотите ждать освобождения заблокированной записи. Для этого в операторе FIND (GET) следует указать опцию NO-WAIT (вместе с NO-ERROR – иначе все равно возникает ошибка). После этого можно использовать одну из двух встроенных функций, чтобы проверить, удалось ли получить доступ к записи. Функция AVAILABLE проверяет доступность буфера записи и возвращает TRUE, если доступ получен. Функция LOCKED в этом же случае возвращает FALSE. И наоборот.
Если же необходимо обработать ситуацию, когда запись не заблокирована, а просто не найдена в базе данных, можно использовать функции AVAILABLE и LOCKED совместно. Если обе функции возвращают FALSE, значит, такой записи в базе данных нет (Программа 9).
Программа 9. Обработка конфликтов блокировки
FIND FIRST Customer WHERE custNum = 1000 EXCLUSIVE-LOCK NO-WAIT NO-ERROR. IF AVAILABLE Customer THEN. /* Access granted. You have a record locked */ ELSE IF LOCKED Customer THEN. /* Access denied. Somebody has record locked */ ELSE. /* Access denied. Record does not exist */
Progress Software утверждает (OpenEdge® Development: Progress® 4GL Handbook by John Sadd – Expert Series), что опция NO-WAIT предназначена для среды, где пользователь напрямую взаимодействует с базой данных, и в «правильно реализованном» распределенном приложении, где все операции с базой данных выполняются сервером (имеется в виду сервер приложения, конечно) не нужна:
«If you code your application properly to make sure that record locks are not held unnecessarily, then lock contention should almost never be an issue».
Для бизнес-системы кодирование в стиле «should almost never» может привести к весьма серьезным проблемам – обрабатывайте конфликты блокировки явно.
Использование режимов блокировок.
Как уже было упомянуто, если Вы просто читаете записи для просмотра, всегда читайте с режимом NO-LOCK. Для генерации отчета также можно использовать NO-LOCK, если допускается попадание в него не полностью завершенных транзакций.
Если не полностью завершенные транзакции не должны показываться / обрабатываться, следует использовать SHARE-LOCK с обработкой конфликтов блокировки. В вышеупомянутой книге, кстати, указано, что опция NO-WAIT может быть указана в операторе FOR EACH, что неверно. Практически, и в этом случае следует в FOR EACH использовать NO-LOCK, а потом пытаться прочитать с SHARE-LOCK и обработать блокирование (Программа 10).
Программа 10. Обработка незаконченных транзакций.
DEFINE BUFFER bCustomer FOR Customer. FOR EACH Customer NO-LOCK: FIND bCustomer WHERE ROWID(bCustomer) = ROWID(Customer)SHARE-LOCK NO-WAIT NO-ERROR. IF AVAILABLE Customer THEN DO: /* Processing */ DISPLAY bCustomer.custNum. END. ELSE NEXT. /* Record locked by somebody: skip */ END.
Режим EXCLUSIVE-LOCK следует указывать при считывании записи непосредственно перед ее обновлением. Техника аналогична показанной для SHARE-LOCK.
Освобождение заблокированных записей.
Ни при каких обстоятельствах Вы не должны сохранять блокирование записи дольше, чем это необходимо. Наилучшей практикой будет явное управление блокированием записей.
Две основных рекомендации:
- Никогда не читайте запись до начала транзакции, даже с NO-LOCK, если вы будете изменять ее внутри транзакции. Если запись прочитана с NO-LOCK до начала транзакции и затем с EXCLUSIVE-LOCK внутри транзакции, по окончании транзакции уровень блокировки будет автоматически понижен до SHARE-LOCK. Progress не восстановит статус NO-LOCK автоматически.
- Если у Вас есть какие-либо сомнения, где заканчивается видимость записи, или когда освобождается блокировка, освобождайте запись явно, после того, как закончили изменения, с помощью оператора RELEASE. После освобождения записи Вы можете быть уверены, что блокировка снята (смотри ниже), и что Вы не можете невольно сослаться на нее после транзакционного блока, что может расширить область действия блокировки, или даже транзакции.
При соблюдении этих правил Ваш код будет намного проще и надежнее. К сожалению, приходится работать и с кодом, не следующим этим правилам. Следует иметь в виду следующее:
- Блокировка SHARE-LOCK сохраняется до конца транзакции или освобождения записи, смотря что случается позднее. Не используйте SHARE-LOCK и всегда явно освобождайте запись после изменения.
- Блокировка EXCLUSIVE-LOCK сохраняется до конца транзакции и затем понижается до SHARE-LOCK, если область видимости записи больше, чем транзакция и запись по-прежнему активна в буфере. Еще раз, освобождение записи гарантирует Вам, что использование записи в транзакции не вызовет конфликта с использованием той же записи вне транзакции.
- Когда Progress откатывает транзакцию, он освобождает блокировки, установленные в процессе обработки транзакции, или изменяет блокировку на SHARE-LOCK, если запись была заблокирована до транзакции. Если вы не устанавливаете блокировку, а лучше даже не читаете запись до начала транзакции, Вам не о чем беспокоится в этом конкретном случае.
Ресурс Lock table
Блокировки SHARE-LOCK и EXCLUSIVE-LOCK отмечаются в специальной таблице, управляемой сервером базы данных. По умолчанию размер таблицы равен 8192. Вы можете установить свое значение с помощью параметра Lock Table Entries (-L n), минимальное значение n равно 32, максимальное зависит от системы (ограничено доступной памятью) и должно быть кратно 32 (автоматически увеличивается до большего).
Переполнение таблицы приводит к системной ошибке и состоянию STOP. При этом сервер продолжает работу. Незавершенные транзакции клиентской сессии откатываются.
Возникновение такой ошибки может означать, что некоторая процедура должна быть реструктурирована с целью уменьшения размера транзакций. Некоторые «административные» процедуры следует выполнять в режиме single‑user. Проверяйте размер транзакций прежде, чем увеличивать размер таблицы блокировок.
Замечание: когда блокировки устанавливаются в блоке с опцией BREAK BY, блокируются две записи.
Оптимистическое и пессимистическое блокирование.
Пессимистическое блокирование, характерное для традиционных архитектур хост-терминал или клиент-сервер, означает, что приложение всегда блокирует записи c EXCLUSIVE-LOCK при чтении записей, которые могут быть изменены, чтобы предотвратить изменение этих же записей другим пользователем.
В распределенных приложениях такой подход просто не работает. Когда Вы читаете запись (или набор записей) на сервере и передаете их клиенту для отображения и возможного изменения, серверная сессия не может просто сохранить блокирование записей, пока клиент использует их. Обычно серверная процедура формирует временную таблицу с данными, возвращает ее клиенту и завершается, освобождая заблокированные записи базы данных (если она вообще их блокировала). В дополнение, Вы просто не можете сохранять блокирование записей на столь длительный период времени, так как это неизбежно приведет к конфликтам блокировки.
Замечание: Это не значит, что невозможно написать серверный код так, что он будет удерживать записи заблокированными после передачи данных клиенту. Но обычно Вы не должны этого делать.
Для распределенных приложений используется оптимистическое блокирование. Это значит, что серверный код всегда читает записи без блокирования (NO-LOCK), если они предаются в клиентскую сессию для отображения или обработки. Когда клиентская сессия изменяет одну или более записей и возвращает их (вероятно, в виде другой копии временной таблицы) серверу, серверная процедура, ответственная за обработку изменений, должна:
- Прочитать каждую измененную запись из базы данных с EXCLUSIVE-LOCK по ключу записи или по RowID.
- Убедиться, что запись не была изменена другим пользователем, или, по крайней мере, что текущие изменения не противоречат другим изменениям.
- Выполнить изменения для записи в базе данных.
Если другой пользователь изменил запись, приложение должно выполнить соответствующие действия. Это может потребовать, например, запрета текущих изменений и передачи измененной записи обратно клиенту для показа и обработки. Или можно каким-то образом объединить два набора изменений, в зависимости от логики приложения и природы данных.
Если Вы имеете запись в буфере записи, Вы можете повторно считать ее из базы данных с помощью оператора FIND CURRENT, или, для query, GET CURRENT. Затем Вы можете сравнить записи в буфере и базе данных с помощью функции CURRENT-CHANGED (см Программа 11).
Программа 11. Оптимистическое блокирование
DEFINE FRAME CustFrame Customer.CustNum Customer.NAME FORMAT "x(12)" Customer.CreditLimit Customer.Balance. ON "GO" OF FRAME CustFrame DO: /* When the user closes the frame by pressing F2, start a transaction: */ DO TRANSACTION: FIND CURRENT Customer EXCLUSIVE-LOCK. IF CURRENT-CHANGED(Customer) THEN DO: MESSAGE "This record has been changed by another user." SKIP "Please re-enter your changes." VIEW-AS ALERT-BOX. DISPLAY Customer.CreditLimit Customer.Balance WITH FRAME CustFrame. RETURN NO-APPLY. /* Cancel the attempted update/GO */ END. /* Otherwise assign the changes to the database record. */ ASSIGN Customer.CreditLimit Customer.Balance. END. RELEASE Customer. END. /* To start out with, find, display, and enable the record with no lock. */ FIND FIRST Customer NO-LOCK. DISPLAY Customer.CustNum Customer.NAME Customer.CreditLimit Customer.Balance WITH FRAME CustFrame. ENABLE Customer.CreditLimit Customer.Balance WITH FRAME CustFrame. /* Wait for the trigger condition to do the update and close the frame. */ WAIT-FOR "GO" OF FRAME CustFrame.
В распределенном приложении обработка таких ситуаций намного сложнее. Процедура, обрабатывающая изменения, обычно не имеет доступа к буферу, хранящему предыдущее состояние записи, и не имеет доступа к пользовательскому интерфейсу. В то же время она должна информировать клиента о конфликте и/или переслать измененные данные. OpenEdge предоставляет Smart-объекты, которые обеспечивают передачу данных от сервера клиенту, поддерживают блокирование записей и прозрачную передачу сообщений и изменений.
На рисунке (Рис. 9) схематично показано взаимодействие Smart Data Object с базой данных и пользовательским интерфейсом.

В данном случае пользовательский интерфейс показан как Smart Data Viewer, что не принципиально.
Естественно, Вы также получаете полнофункциональный API для работы с вашим объектом.
В то же время Smart-объекты представляют собой код на ABL – и разработчик при желании может обеспечить аналогичную функциональность в собственных процедурах.