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

Ответ

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

  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;

Ответ 18+ 🔞

А, ну это же классика, ёпта! Ситуация, когда два программиста одновременно лезут в одну и ту же базу, как два мужика в один холодильник за последним пивом. Начинается натуральный пиздец, который учёные мужи красиво называют «аномалиями изоляции». Слушай, тут просто овердохуища проблем может вылезти.

Вот представь, два процесса, оба такие важные, оба хотят обновить одни и те же цифры. А СУБД смотрит на это и думает: «Ну, блядь, сейчас будет весело». И первая же беда — потерянное обновление. Это когда оба прочитали одно и то же значение, оба посчитали своё, а потом второй, такой наглый, своим коммитом просто затирает результат первого, будто его и не было. В итоге работа первого чувака накрылась медным тазом. Как в том примере с балансом:

-- Чувак 1: Видит balance = 100, хочет добавить 50.
-- Чувак 2: Видит balance = 100, хочет снять 20.
-- Если всё пойдёт по пизде, то в итоге будет либо 80, либо 150, а не правильные 130.

Дальше — грязное чтение. Это вообще мрак. Одна транзакция начиталась данных, которые вторая ещё даже не закоммитила, а та взяла и откатилась! И первый остаётся с цифрами, которые хуй с горы, их никогда не существовало. Доверия к таким данным — ноль ебать.

Потом идёт неповторяющееся чтение. Вообще смешно. Ты в рамках одной своей операции два раза подряд запрашиваешь одни и те же данные, а они, сука, разные! Потому что между твоими запросами какой-то хитрожопый конкурент успел их поменять и сохранить. Ты просто сам от себя охуеешь: «Я что, ослеп? Только же было 100, а теперь 80!».

Ну и вишенка на торте — фантомное чтение. Похоже на предыдущее, но ещё более подлое. Ты смотришь, скажем, на список заказов за сегодня, видишь 5 штук. Смотришь через секунду — а их уже 6! Или 4! Откуда взялась новая строка или куда делась старая? Да просто другая транзакция параллельно впендюрила или удалила запись. Чувствуешь себя обманутым, как лох.

И как со всем этим бороться, спросишь ты? А вот как, чувак.

Во-первых, уровни изоляции транзакций. Это как настройка степени паранойи в системе. Можно поставить REPEATABLE READ или вообще SERIALIZABLE — тогда СУБД будет так жестко контролировать доступ, что у конкурентов волнение ебать начнётся, и они будут ждать своей очереди, как в хорошей поликлинике. Правда, производительность иногда просит чих-пых тебя в сраку, но зато данные целы.

А во-вторых, есть метод для настоящих мужчин — пессимистичные блокировки. Ты прямо говоришь базе: «Смотри, я сейчас буду работать с этой строкой, и пока я не закончу, все остальные пусть идут на хуй и ждут». Делается это через SELECT ... FOR UPDATE. Красота!

Вот как это выглядит в коде:

BEGIN TRANSACTION;
-- Говорим: "Эта строка с id=1 теперь моя, блядь, не трогать!"
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- ... Тут спокойно считаем, думаем, пьём кофе ...
-- ... Потом делаем UPDATE ...
COMMIT;

Пока ты не сделаешь COMMIT, любой другой, кто попробует сделать такой же SELECT FOR UPDATE на эту запись, просто повиснет в ожидании. Терпения у него, конечно, ноль ебать, но зато ты избежишь всех этих ебушки-воробушки с потерянными обновлениями и фантомами.

Короче, суть в том, чтобы не пускать всех в одну раздевалку одновременно. Либо доверяешь системе (уровни изоляции), либо сам всех стро́ишь в очередь (блокировки). И тогда твои данные будут в безопасности, а не как у того Гамлета — все ходят и меняют что хотят.