
Стратегии блокировки записей
Блокировка записи представляет собой методологию, направленную на предотвращение одновременного доступа к данным в базе данных с целью исключения возникновения противоречивых результатов.
Блокировка записей является неотъемлемым компонентом любой системы, функционирующей в режиме многопользовательского доступа. В данном контексте рассмотрим несколько подходов к управлению многопользовательским доступом к базе данных.
Немедленная блокировка
Один из методов блокировки заключается в том, чтобы блокировать запись сразу после начала процесса редактирования. Обычно это происходит, когда пользователь выбирает пункт меню редактирования после нахождения нужной записи. В старых приложениях подобные операции были весьма распространены, поскольку доступ к другим частям системы обычно ограничивался до завершения редактирования. Этот метод гарантировал, что в текущий момент времени доступ к редактированию будет предоставлен только одному пользователю.
Однако такой подход сопряжён с определёнными трудностями, которые существенно обострились с возникновением графических интерфейсов и приложений, управляемых событиями.
Вероятность возникновения длительной блокировки записи. В случае, если пользователь покидает своё рабочее место до завершения редактирования, запись остаётся в заблокированном состоянии до его возвращения. В течение этого периода любой другой пользователь, требующий доступа для редактирования данной записи, вынужден ожидать её разблокировки, что фактически приводит к приостановке его рабочих процессов. Для определённых записей такое состояние блокировки может создавать значительные трудности и представлять собой критическую проблему.
Проведение открытой транзакции во время взаимодействия с пользователем. Для того чтобы обеспечить эксклюзивную блокировку, необходимо открыть транзакцию. Если приложение предоставляет возможность переключения пользователя от операции редактирования к другой части приложения, как это принято в графических приложениях, то все действия, выполняемые в другой части, будут рассматриваться как субтранзакции к текущей активной транзакции, начатой во время операции редактирования.
Немедленная псевдоблокировка
Псевдоблокировка представляет собой промежуточный вариант между немедленной и оптимистической блокировками, который позволяет блокировать запись исключительно в момент начала редактирования, после чего уровень блокировки снижается до общего (Share-Lock). Реализация такой блокировки в виде атомарной операции перед взаимодействием с пользователем создаёт очень короткое окно транзакции.
Этот подход также может открыть длинное транзакционное окно, создавая конфликты доступа, но он предотвращает включение несвязанных действий в транзакцию в приложении, управляемом событиями.
Оптимистическая блокировка
На противоположном конце спектра находится стратегия оптимистической блокировки. В рамках этой стратегии запись не блокируется до тех пор, пока пользователь не попытается зафиксировать изменения. Это приводит к следующим последствиям:
- Несколько пользователей могут одновременно вносить изменения в одну и ту же запись, что может вызвать конфликт при фиксации.
- Проблемы, связанные с немедленной блокировкой, устраняются, поскольку блокировка не применяется, и транзакция не является активной.
Данный метод называется оптимистическим, поскольку приложение исходит из предположения, что конфликт не возникнет. Степень достоверности этого предположения варьируется в зависимости от конкретного приложения и его контекста. В любом случае, независимо от частоты конфликтов, приложение должно быть оснащено механизмами их обработки.
Перед фиксацией изменений приложение должно проверять, были ли внесены изменения в запись. Если запись не изменялась, фиксация может быть выполнена в обычном порядке. Если запись была изменена, для второго пользователя возникнет конфликт фиксации. В этом случае типичным ответом будет информирование пользователя о проблеме и предложение либо перезагрузить новую запись для проверки (что приведет к потере правок второго пользователя), либо разрешить фиксацию, перезаписав правки первого пользователя. Также возможен третий подход: перезагрузка, которая объединяет записи и обрабатывает конфликты на уровне полей. Третий вариант более сложен в реализации из-за ограничений визуализации: как отобразить конфликтующие данные в пространстве, где обычно представлено только одно представление.
Выбор между подходами зависит от специфики приложения. Однако в любом случае важно идентифицировать проблемные области и предусмотреть их обработку.
Как реализовать оптимистическую стратегию блокировки
При разработке приложения на языке ABL (4GL) для развертывания в многопользовательской среде необходимо разработать стратегию управления блокировкой записей, которая позволит минимизировать время блокировки записей. Рекомендуется использовать оптимистическую стратегию блокировки, при которой доступ к записям вначале осуществляется с использованием опции NO-LOCK, а затем, в случае необходимости, применяется EXCLUSIVE-LOCK на короткий промежуток времени.
В следующем примере рассматривается работа с базой данных Sports2000 в двух сессиях. В первой сессии процедура получает доступ к записи с использованием оператора FIND и опций EXCLUSIVE-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 в программный блок, содержащий операцию обращения к записи.
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().
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.
Для проверки состояния блокировки записи в циклической конструкции DO WHILE можно воспользоваться функцией LOCKED.
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, который может быть использован для выявления блокировок записи в таблице:
/* findLock.p Находит пользователя, который блокирует запись заставляя других ждать.*/ 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 " Пользователи, блокирующие записи других ползователей ": 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.
Весь программный код, представленный в данной статье, предоставляется в исходном виде без каких-либо гарантий его пригодности для конкретных целей. Пользователям настоятельно рекомендуется тщательно изучить и понять функциональность и потенциальные последствия использования кода перед его применением в производственной среде.
Метка:ABL(4GL), Database, Tuning Progress 4GL