Содержание

Предыдущий раздел

Кеширование результата запроса в PL/pgSQL функции

Следующий раздел

Возврат данных из анонимного DO-блока

Ошибки при работе с рекомендательными (advisory) блокировками

Рекомендательные блокировки (РБ) - это удобный механизм (на уровне бизнес-логики) для организации конкурентного доступа к данным. РБ отличаются от стандарных блокировок тем, что они не являются обязательными и никак не взаимодействует с 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)
dec_balance(1, 100)dec_balance(1, 99)Параллельно запущены две процедуры по списанию денег с одного аккаунта
locklockБлокировка на одном и том же ресурсе. Блокировка в С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, который бы мог помочь гарантировать освобождение блокировки в случае возникновения исключительной ситуации.

comments powered by Disqus