Что может пойти не так при работе нескольких запросов с одними и теми же данными, если есть 2 транзакции, которые их обновляют?

«Что может пойти не так при работе нескольких запросов с одними и теми же данными, если есть 2 транзакции, которые их обновляют?» — вопрос из категории Базы данных, который задают на 28% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

При параллельном выполнении двух транзакций, обновляющих одни и те же данные, могут возникнуть классические проблемы параллелизма, которые СУБД описывает через аномалии изоляции:

  1. Потерянное обновление (Lost Update): Две транзакции читают одну строку, вносят изменения на основе прочитанного и коммитят. Изменения второй транзакции полностью перезаписывают изменения первой.

    -- Транзакция 1: Читает balance = 100, хочет добавить 50.
    -- Транзакция 2: Читает balance = 100, хочет вычесть 20.
    -- Если они коммитятся в неправильном порядке, результат может быть 80 или 150, вместо корректного 130.
  2. Грязное чтение (Dirty Read): Транзакция 1 читает незакоммиченные данные из транзакции 2. Если транзакция 2 откатывается, транзакция 1 работает с несуществующими данными.

  3. Неповторяющееся чтение (Non-repeatable Read): В рамках одной транзакции два одинаковых запроса возвращают разные данные, потому что между ними другая транзакция изменила и закоммитила эти строки.

  4. Фантомное чтение (Phantom Read): Похоже на неповторяющееся чтение, но касается появления или исчезновения новых строк, удовлетворяющих условию запроса, из-за действий другой транзакции.

Способы предотвращения:

  • Использование уровней изоляции транзакций (например, REPEATABLE READ или SERIALIZABLE).
  • Применение пессимистичных блокировок (например, SELECT ... FOR UPDATE), которые явно блокируют строки для изменения.

Пример пессимистичной блокировки в SQL:

BEGIN TRANSACTION;
-- Блокируем строку для обновления, чтобы другие транзакции ждали
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- ... выполняем вычисления и UPDATE ...
COMMIT;