Стратегии блокировки записей
Блокировка записи — это метод предотвращения одновременного доступа к данным в базе данных, чтобы предотвратить противоречивые результаты.
Блокировка записей является важной частью любой многопользовательской системы. Рассмотрим несколько подходов к управлению многопользовательским доступом к базе данных.
Немедленная блокировка
Один из подходов к блокировке заключается в блокировке записи сразу после начала редактирования. Это обычно делается, когда пользователь выбрал пункт меню редактирования после нахождения нужной записи. В старых символьных приложениях такие операции были обычным делом, поскольку доступ к другим частям системы обычно ограничивался до завершения редактирования. Этот метод гарантировал доступ к редактированию для одного человека в текущий момент времени.
Однако этот подход имеет некоторые проблемы, которые были значительно усугублены появлением графических интерфейсов и управляемых событиями приложений:
- Вероятность возникновения длительной блокировки записи. Например, если пользователь ушёл на обед без завершения редактирования, то запись останется блокированной до тех пор, пока пользователь не вернётся и не завершит транзакцию. До этого момента любой другой пользователь, которому необходим доступ для редактирования к этой записи, будет вынужден ожидать её освобождения, фактически его работа тоже будет блокирована. Для некоторых записей такое блокированное состояние может быть критичной проблемой.
- Проведение открытой транзакции во время взаимодействия с пользователем. Транзакция должна быть открыта для того, чтобы удерживать эксклюзивную блокировку. Если приложение поддерживает возможность переключения пользователя от операции редактирования к другой части приложения, как это обычно происходит в графическом приложении, то всё, что делается в другой части будет рассматриваться как субтранзакция к текущей активной транзакции, начатой во время операции редактирования.
Немедленная псевдоблокировка
Псевдоблокировка – это промежуточный подход между немедленной и оптимистической блокировками, которая позволяет блокировать запись исключительно в момент начала редактирования, после чего её уровень блокировки понижается до общей (Share-Lock). Реализация такой блокировки как атомарной операции перед взаимодействием с пользователем создаёт очень маленькое окно транзакции.
Этот подход также потенциально может открыть длинное транзакционное окно создавая конфликты доступа, но он предотвращает включение несвязанных действий в транзакцию в управляемом событиями приложении.
Оптимистическая блокировка
На другом конце спектра – оптимистическая блокировка. При реализации оптимистической блокировки запись не блокируется до тех пор, пока пользователь не предпримет действия по фиксации записи. Это оказывает следующее влияние:
- Одновременно несколько пользователей могут редактировать одну и ту же запись, что приводит к конфликту фиксации.
- Обе проблемы, связанные со стратегией немедленной блокировки, устраняются, поскольку блокировка не была произведена и транзакция не активна.
Этот метод называется оптимистическим, поскольку приложение предполагает, что никакого конфликта не произойдёт. Степень истинности этого предположения сильно варьируется в зависимости от каждого приложения и области приложения. В любом случае, независимо от частоты конфликтов, в приложении должна быть реализована обработка конфликтов, в случае их возникновения.
Перед каждой фиксацией приложение должно проверить, была ли изменена запись. Если запись не была изменена, фиксация может продолжаться в обычном режиме. Если запись была изменена, то для второго пользователя произойдёт конфликт фиксации. В этом случае типичным ответом будет сообщение пользователю о проблеме и предложение либо перезагрузить новую запись для проверки, что приведёт к потере правок второго пользователя, либо разрешить фиксацию в любом случае, которая перезапишет правки первого пользователя. Возможен и третий подход: перезагрузка, которая объединяет записи и обрабатывает конфликты по полям. Третий вариант сложнее запрограммировать из-за ограничений визуализации: как показать конфликтующие данные в пространстве, где обычно существует только одно представление.
Будет ли один подход лучше другого, зависит от приложения. Но независимо от подхода, важно распознать и запланировать обработку проблемной области.
Как реализовать оптимистическую стратегию блокировки для управления блокировкой записей
При разработке ABL(4GL) приложения для развёртывания в многопользовательской среде вы должны разработать стратегию блокировки записей, которая позволит избежать ненужной блокировки в течение длительного периода времени. Для достижения этой цели рекомендуется оптимистическая стратегия, при которой доступ к записям первоначально осуществляется с помощью NO-LOCK, а затем в течение очень короткого периода времени применяется EXCLUSIVE-LOCK.
В следующем примере с базой данных Sports2000 работают две сессии. В первой сессии процедура получает доступ к записи с EXLUSIVE-LOCK, в то время как процедура второй сессии пытается получить доступ к той же записи используя оптимистическую стратегию.
В первой процедуре для доступа к записи с помощью оператора FIND используются опции EXLUSIVE-LOCK NO-WAIT NO-ERROR. Благодаря которым возможность изменения записи появится у пользователя только если запись доступна и не блокирована. При этом, если запись кем-то заблокирована, процедура не будет ожидать её разблокировки благодаря опции NO-WAIT.
// Процедура 1, немедленная блокировка записи. FIND customer WHERE customer.custnum = 1 EXCLUSIVE-LOCK NO-WAIT NO-ERROR. // Если запись доступна, то немедленно приступить к обновлению (активная транзакции) IF AVAILABLE customer THEN DO: DISPLAY customer.custnum customer.name WITH 1 COLUMN. UPDATE customer.name. END. PAUSE.
Во второй процедуре сначала выполняется поиск записи с опциями NO-LOCK NO-ERROR, и если запись обнаружена, то её содержимое выводится на экран, а пользователю предлагается её отредактировать. В случае если пользователь ответит положительно, то во втором операторе FIND будут использованы опции EXCLUSIVE-LOCK NO-ERROR NO-WAIT для того, чтобы увеличить уровень блокировки с NO-LOCK до EXCLUSIVE-LOCK. При этом проверяются доступность записи (IF AVAILABLE customer) и изменилась ли эта запись с момента первого поиска (IF CURRENT-CHANGED customer). Если запись изменилась, то пользователю выводится обновлённое содержимое записи после чего предлагается её изменить. Обратите внимание, что если запись недоступна (IF NOT AVAILABLE customer), то дополнительно выполняется проверка почему она не доступна. Здесь может быть два варианта. Первый, запись заблокирована кем-то (IF LOCKED customer), и второй, запись была удалена.
// Процедура 2, Пример оптимистической блокировки FIND customer WHERE customer.custnum = 1 NO-LOCK NO-ERROR. IF AVAILABLE customer THEN DISPLAY Customer.custnum customer.NAME WITH 1 COLUMN. PAUSE. MESSAGE "Вы хотите изменить эту запись?" VIEW-AS ALERT-BOX QUESTION BUTTONS YES-NO UPDATE upd AS LOGICAL. IF upd THEN DO: // Попытка получить блокировку на записи FIND CURRENT customer EXCLUSIVE-LOCK NO-ERROR NO-WAIT. IF AVAILABLE customer THEN //Доступна ли запись? IF CURRENT-CHANGED customer THEN //Была ли изменена запись? DO: DISPLAY Customer.custnum customer.NAME WITH 1 COLUMN. UPDATE customer.NAME. END. ELSE UPDATE customer.NAME. ELSE IF NOT AVAILABLE customer THEN //Запись не доступна? IF LOCKED customer THEN //Запись заблокирована MESSAGE "Эта запись заблокирована другим пользователем." VIEW-AS ALERT-BOX. ELSE MESSAGE "Эта запись была удалена." VIEW-AS ALERT-BOX. END.
Как обработать условие, вызванное таймаутом ожидания блокировки
При попытке блокировки записи и обнаружении, что запись уже заблокирована, в зависимости от того, как был написан код, сессия может оказаться в состоянии ожидания, которое будет длиться до тех пор, пока не истечёт интервал, указанный параметром клиентской сессии -lkwtmo.
По умолчанию, если параметр явно не указан, его значение равно 1800 секунд (30 минут). Но скорее всего этого может быть слишком много для вашего приложений. Поэтому вы можете установить своё значение, например минимальные 10 секунд (-lkwtmo 10) в параметрах клиентских сессий, или добавить его в общий файл параметров DLC/startup.pf.
По истечении интервала -lkwtmo, если запись все ещё заблокирована другим пользователем, в программе, в которой была предпринята попытка блокировки, возникает условие остановки (STOP). Это условие может остановить дальнейшее выполнение этой программы, однако как правило желательно, чтобы процесс продолжал выполняться.
Рассмотрим несколько вариантов обработки условия остановки, чтобы избежать завершения выполнение программы.
Добавьте условие ON STOP в блок, который содержит оператор обращения к записи.
// Процедура 3, применение ON STOP DO ON STOP UNDO, LEAVE: FIND FIRST customer EXCLUSIVE-LOCK. END. MESSAGE "Запись доступна? " AVAILABLE(customer) VIEW-AS ALERT-BOX INFO BUTTONS OK.
Благодаря условию ON STOP на уровне блока, пользователь увидит сообщение по истечение интервала -lkwtmo вместо простого завершения программы.
Однако этот метод слишком долгий. Пользователь может не дождаться истечения интервала, и не понимая, что происходит, сам завершить работу программы.
Поэтому мы изменим код чтобы быть более дружественными к пользователю. Для этого используем дополнительные опции NO-WAIT NO-ERROR для оператора FIND, а также задействуем функцию времени выполнения ETIME().
// Процедура 4, проверка доступности записи в цикле REPEAT ETIME(TRUE). REPEAT: //Выполнять FIND пока запись не будет разблокирована или не истечёт интервал 10 секунд (10000 миллисекунд = 10 секунд) FIND FIRST customer EXCLUSIVE-LOCK NO-WAIT NO-ERROR. IF NOT LOCKED customer OR ETIME GT 10000 THEN LEAVE. END. IF LOCKED customer THEN DO: MESSAGE " Запись заблокирована" VIEW-AS ALERT-BOX. LEAVE. END.
Мы можем использовать функцию LOCKED для проверки блокировки записи в цикле DO WHILE.
// Процедура 5, проверка доступности записи в цикле DO WHILE FIND Customer EXCLUSIVE-LOCK NO-ERROR NO-WAIT. IF NOT AVAILABLE Customer THEN DO: IF LOCKED Customer THEN DO WHILE LOCKED Customer: FIND Customer EXCLUSIVE-LOCK NO-ERROR NO-WAIT. END. END.
Таким образом, одной из главных задач разработчика приложения должно быть снижение конкуренции за записи путём сокращения времени блокировки настолько, насколько это возможно.
Для получения дополнительной информации по работе с блокировками рекомендую обратиться к документации по ссылке.
Данная статья будет не полной, если не упомянуть о том, как программным способом обнаружить кто является блокировщиком записи в таблице. Это важно, потому что, когда пользователь или, тем более, фоновый процесс, пытается получить блокировку для записи, которая уже заблокирована другим пользователем, администратору базы данных может быть проблематично выяснить, кто удерживает первоначальную блокировку. К сожалению, часто приложение не сообщает пользователю о том, кто действительно удерживает блокировку в данный момент, потому что обработка подобных ситуаций не была хорошо продумано разработчиком. Поэтому администратору базы данных может быть непросто выяснить, кто на самом деле является первоначальным «виновником». Далее приведён фрагмент ABL-кода, который поможет в этой ситуации.
// Процедура 5, поиск пользователя, который блокирует запись заставляя других ждать. DEFINE TEMP-TABLE ttLocks NO-UNDO LIKE _Lock INDEX byRecid IS PRIMARY _Lock-RecId ASCENDING _Lock-Table ASCENDING. DEFINE BUFFER culprit FOR ttLocks. // Доступ к системной таблице _Lock происходит медленно, // поэтому сначала скопируем _Lock в индексированную временную таблицу ttLocks. FOR EACH _Lock WHILE _Lock._Lock-table <> ?: CREATE ttLocks. BUFFER-COPY _Lock TO ttLocks. END. FOR EACH ttLocks WHERE ttLocks._Lock-Flags MATCHES "*Q*": // Делаем FIND с NO-ERROR, так как таблица блокировки – это всего лишь снимок очень изменчивых данных... FIND FIRST culprit WHERE culprit._Lock-RecId = ttLocks._Lock-RecId AND culprit._Lock-Table = ttLocks._Lock-Table AND NOT culprit._Lock-Flags MATCHES "*Q*" NO-ERROR. IF AVAILABLE culprit THEN DO WITH SIDE-LABELS TITLE " Users holding other users records ": FIND _Connect WHERE _Connect._Connect-Usr = culprit._Lock-Usr NO-ERROR. IF AVAILABLE _Connect AND _Connect._Connect-TransId <> 0 THEN FIND _Trans WHERE _Trans._Trans-Usr = _Connect._Connect-Usr NO-ERROR. ELSE // Убедимся, что запись _Trans доступна. RELEASE _Trans NO-ERROR. FIND _File WHERE _File._File-num = culprit._Lock-Table NO-LOCK NO-ERROR. DISPLAY culprit._Lock-Usr COLON 17 culprit._Lock-Name COLON 17 _Connect._Connect-Device WHEN AVAILABLE _Connect LABEL "On" culprit._Lock-Table COLON 17 LABEL "Table" FORMAT "ZZ,ZZ9" _File._File-Name WHEN AVAILABLE _File NO-LABEL culprit._Lock-RecID COLON 17 culprit._Lock-Type COLON 17 LABEL "Lock type" culprit._Lock-Flags . IF AVAILABLE _Trans THEN DISPLAY _Trans._Trans-State COLON 17 _Trans._Trans-Txtime COLON 17 "Transaction start" . IF AVAILABLE _Connect THEN DISPLAY _Connect._Connect-Type COLON 17 LABEL "Client type" _Connect._Connect-Time COLON 17 . END. END.
P.S.
Весь код, который приведён в этой статье предоставляется как есть, без каких-либо гарантий для той или иной цели. Пользователям настоятельно рекомендуется ознакомиться и понять, что делает код, прежде чем использовать его в производственной среде.
Метка:ABL(4GL)