Рекомендательные блокировки (РБ) - это удобный механизм (на уровне бизнес-логики) для организации конкурентного доступа к данным. РБ отличаются от стандарных блокировок тем, что они не являются обязательными и никак не взаимодействует с MVCC (ключевым механизмом конкурентного доступа к данных в PostgreSQL). Таким образом, РБ являются вспомогательным механизмом, с помощью которого приложения могут контролировать доступ к какому-то ресурсу и этим ресурсом может быть не обязательно таблица.
РБ могут работать на уровне сессий и на уровне транзакций (начиная с 9.1).
РБ уровня сессии часто используются вместо блокировок уровня транзакции. Это неправильный подход, так он ведет к возникновению ошибок, которые сложно обнаружить.
К примеру, есть процедура, которая производит списание денег с расчетного счета. Для контроля конкурентного доступа используются РБ уровня сессии.
Пример кода хранимой процедуры
-- FUNCTION dec_balance
DECLARE
_account_id int ALIAS FOR $1; -- Идентификатор счета
_value numeric ALIAS FOR $2; -- Сумма, которую надо списать
_balance numeric;
_ACCOUNT_LOCK_ID CONSTANT int := 2;
BEGIN
PERFORM pg_advisory_lock(_ACCOUNT_LOCK_ID, _account_id); -- Блокировка счета
SELECT balance INTO _balance FROM accounts WHERE id = _account_id; -- Получение текущего баланса счета
IF _balance >= _value THEN -- Проверка допустимости списания
UPDATE accounts SET balance = balance - _value WHERE id = _account_id; -- Обновление баланса
END IF;
PERFORM pg_advisory_unlock(_ACCOUNT_LOCK_ID, _account_id); -- Освобождения блокировки
END
На первый взгляд всё хорошо - блокировка устанавливается до каких-либо проверок и освобождается только после внесения изменений в таблицу. Где тут ошибка? Ошибка в том, что РБ уровня сессии освобождается чуть раньше завершения самой транзакции. И в этот промежуток времени между освобождением блокировки и завершением транзакции может произойти что угодно (и пройти много времени).
Ниже приводится пример, когда выше приведенный код будет работать некорректно
Сессия 1 (C1) | Сессия 2 (C2) | Комментарий |
---|---|---|
BEGIN | Стартовала транзакция в первой сессии (С1) | |
BEGIN | Стартовала транзакция во второй сессии (С2) | |
Параллельно запущены две процедуры по списанию денег с одного аккаунта | ||
lock | lock | Блокировка на одном и том же ресурсе. Блокировка в С1 оказалась чуть быстрее, поэтому выполнение кода в С2 приостановилось до освобождения блокировки |
check_balance | Проверка текущего баланса (он, к примеру, равен 101) | |
update_balance | Денег на счету достаточно. Присходит списание | |
unlock | Деньги списались. Можно освободить блокировку. | |
check_balance | В С2 происходит проверка баланса. Но так как транзакция в С1 ещё не завершилась, то транзакция в С2 не видит изменений, сделанных в С1. Поэтому проверка завершается успешно. | |
update_balance | Происходит списание денег со счета. С2 блокируется до завершения С1, так как произошло конкуретное изменение записи, которая была изменена в С1 и результат изменения ещё не зафиксирован в базе. Работает механизм MVCC. | |
COMMIT | Транзакция в С1 завершается. | |
unlock | С2 разблокируется, и освобождает блокировку | |
COMMIT | Транзакция в С2 завершена. |
Итогом выполнения этих двух транзакций будет минусовой (-97) баланс счета. А этот вариант развития событий как раз и должна была предотвратить используемая блокировка (advisory lock). Но она не сработала, так как она была освобождена раньше завершения транзакции в С1 и транзакция в С2 не увидела изменения сделанные в С1 (см. как работают уровни изоляции транзакций).
На основе выше приведенного примера можно сделать вывод, что для корректной работы РБ уровня сессии, необходимо их освобождать либо одновременно с завершением транзакции (то есть использовать блокировки уровня транзакции), либо после окончания транзакции (эта ответственность ляжет на клиентский код). Только в этом случае будет гарантия, что РБ уровня сессии сработают корректно.
А лучше вообще не используйте РБ уровня сессии для контроля конкурентного доступа к транзакционным ресурсам - для этого есть удобные механизмы (РБ уровня транзакции и стандартные блокировки), которые именно для этого и созданы.
Примечание
При откате транзакции РБ уровня сессии не освобождаются, поэтому надо быть очень осторожным при использовании этих блокировок в хранимых процедурах. К примеру, в в PL/pgSQL процедурах нет блока finally, который бы мог помочь гарантировать освобождение блокировки в случае возникновения исключительной ситуации.