В приложении две сессии выполняют следующий SQL. Объясните, почему оператор во второй сессии может ждать завершения первой транзакции, хотя он только читает данные.
-- Подготовка
CREATE TABLE accounts (
id integer PRIMARY KEY,
balance numeric NOT NULL
);
-- Сессия 1
BEGIN;
SELECT * FROM accounts WHERE id = 10 FOR UPDATE;
-- транзакция ещё не завершена
-- Сессия 2
BEGIN;
SELECT * FROM accounts WHERE id = 10 FOR UPDATE;
FOR UPDATE выполняет блокирующее чтение: первая транзакция получает блокировку выбранной строки, поэтому вторая транзакция ждёт её освобождения. Такая блокировка нужна, чтобы прочитанная строка не была изменена другой транзакцией до окончания критической секции.
Обычного чтения недостаточно для сценария «прочитать текущую запись, принять решение, затем изменить её». Между чтением и изменением другая транзакция может поменять ту же строку, поэтому СУБД поддерживают пессимистические блокировки: конфликт предполагается заранее, а доступ к данным временно сериализуется.
SELECT ... FOR UPDATE возник как практический способ явно обозначить намерение изменить найденные строки. Это позволяет не блокировать все чтения подряд, а блокировать только строки, участвующие в критической операции.
В первой сессии строка с id = 10 выбирается с блокировкой изменения. Пока транзакция не выполнит COMMIT или ROLLBACK, другая транзакция не может получить несовместимую блокировку на ту же строку.
Если вторая сессия не будет ждать, она сможет принять решение по устаревающему состоянию или одновременно изменить тот же объект. Если же транзакция удерживает блокировку слишком долго, возрастут задержки, появится риск тайм-аутов и снизится пропускная способность.
FOR UPDATE означает не «прочитать с повышенной точностью», а запросить блокировку, совместимую с последующим изменением строки. Первая сессия получает такую блокировку. Вторая сессия пытается получить конфликтующую блокировку и обычно переходит в ожидание.
Блокировка обычно удерживается до конца транзакции, а не до конца отдельного SELECT. Поэтому следующий код освобождает строку только после COMMIT:
После фиксации первой транзакции вторая сессия продолжает выполнение. В зависимости от СУБД и уровня изоляции она должна получить корректное актуальное состояние строки для блокирующего чтения; конкретные детали видимости версий относятся к реализации MVCC.
Если строка не существует, поведение зависит от СУБД, индексов и уровня изоляции: блокировка самой строки невозможна, поэтому для защиты условия по диапазону могут потребоваться блокировки диапазона или уровень SERIALIZABLE. FOR UPDATE также не заменяет проверку бизнес-условий после блокировки.
Пессимистическая блокировка полезна при высокой вероятности конфликта, но уменьшает параллелизм. Важно держать транзакцию короткой, выбирать строки в одинаковом порядке и задавать тайм-ауты, чтобы снизить риск длительных ожиданий и взаимных блокировок.
Сервис переводов сначала должен проверить баланс счёта, затем списать деньги. Без блокирующего чтения два параллельных перевода могут оба прочитать один и тот же баланс и принять решение на основании состояния, которое уже меняется.
Рассматривались три варианта:
UPDATE с условием — обычно даёт высокий параллелизм и не требует отдельной блокировки, но сложнее, если операция включает несколько таблиц или много проверок.version; конфликт обнаруживается без ожидания, но приложение должно повторять операцию или возвращать ошибку.SELECT ... FOR UPDATE — логика проверки остаётся простой, а конфликтующие операции выстраиваются в очередь; недостаток — ожидание блокировки и меньшая пропускная способность при конкуренции.Для перевода с несколькими последовательными проверками выбран FOR UPDATE. Сервис блокирует только нужный счёт, выполняет проверку и списание в одной короткой транзакции, затем сразу фиксирует результат. Это обеспечивает последовательное принятие решений по конкретному счёту без блокировки всей таблицы.
1. Освобождается ли блокировка сразу после завершения SELECT ... FOR UPDATE?
Нет, в типичной транзакционной модели она удерживается до COMMIT или ROLLBACK. Это необходимо: если снять блокировку сразу после чтения, другая транзакция сможет изменить строку до выполнения последующего UPDATE, и защищённая критическая секция распадётся.
Исключения и детали зависят от СУБД: некоторые режимы могут освобождать отдельные блокировки после сохранения точки или отката к ней. На прикладной код нельзя переносить такое предположение без проверки документации конкретной СУБД.
2. Гарантирует ли FOR UPDATE блокировку всей таблицы?
Нет. Обычно блокируются найденные строки, а СУБД дополнительно может использовать блокировки страниц, диапазонов или другие внутренние блокировки. Если запрос не находит строку, блокировать нечего, и защита условия вроде «в диапазоне нет записей» может отсутствовать.
Поэтому для инвариантов, зависящих от отсутствующих строк или диапазона, нужны подходящие ограничения, уникальные индексы либо более строгий механизм изоляции. Выбор должен учитывать конкретную СУБД и план выполнения.
3. Можно ли считать ожидание второй сессии ошибкой?
Не обязательно. Ожидание — нормальный результат разрешения конфликта: операции получают последовательный доступ к одной строке. Ошибкой оно становится, если транзакции удерживают блокировки слишком долго, возникают тайм-ауты или система допускает циклическое ожидание.
Практически измеряют время ожидания, ограничивают длительность транзакций и обрабатывают ошибки блокировок. Для операций с допустимым повтором иногда предпочтительнее оптимистический контроль версий, поскольку он не удерживает очередь блокирующих читателей.