Что такое конкуренция транзакций в базах данных?

Ответ

Конкуренция транзакций — это ситуация, когда несколько параллельных транзакций пытаются получить доступ (чтение или запись) к одним и тем же данным в базе данных, что может привести к проблемам согласованности.

Основные проблемы (аномалии)

  1. Потерянное обновление (Lost Update): Две транзакции читают одну запись, обе изменяют её, но сохраняется только результат последнего коммита, перезаписывая изменения первой.
  2. Грязное чтение (Dirty Read): Транзакция читает незафиксированные (и потенциально откатываемые) данные другой транзакции.
  3. Неповторяющееся чтение (Non-repeatable Read): В рамках одной транзакции повторное чтение одной и той же записи даёт разные результаты, потому что другая транзакция изменила и зафиксировала её.
  4. Фантомное чтение (Phantom Read): Повторное выполнение запроса с условием возвращает разный набор строк из-за того, что другая транзакция добавила или удалила записи, удовлетворяющие условию.

Пример (Lost Update)

// Транзакция 1
entityManager.getTransaction().begin();
Product p1 = entityManager.find(Product.class, 1L); // Читает price = 100
// Транзакция 2 в это время:
// Product p2 = entityManager.find(Product.class, 1L);
// p2.setPrice(p2.getPrice() + 50); // price = 150
// entityManager.getTransaction().commit();
p1.setPrice(p1.getPrice() + 100); // Ожидает price = 100, устанавливает 200
entityManager.getTransaction().commit(); // Перезаписывает цену 150 на 200

Решения

  • Уровни изоляции транзакций (от низкого к высокому): READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE. Каждый следующий уровень предотвращает определённый набор аномалий, но может снижать производительность.
  • Механизмы блокировок:
    • Пессимистичные: Данные блокируются на время транзакции (SELECT ... FOR UPDATE).
    • Оптимистичные: Используется контроль версий (например, поле version). При коммите проверяется, не изменились ли данные с момента чтения, в случае конфликта транзакция откатывается.

Ответ 18+ 🔞

Давай разберём эту тему, а то я вижу, у тебя глаза уже стекленеют от этих умных слов. Конкуренция транзакций — это когда несколько твоих запросов в базу начинают драться за одну и ту же запись, как два мужика за последнюю бутылку в ларьке после зарплаты. Итог предсказуем: пиздец и разбитые надежды.

Чего может пойти не по плану (или классика жанра «Я ж думал, всё норм»)

  1. Потерянное обновление (Lost Update). Представь: ты и твой кореш смотрите на цену товара в базе. Видите «100 рублей». Ты такой: «О, надо накинуть сотку!». А он: «Надо накинуть полтинник!». Ты быстренько меняешь на 200 и сохраняешь. А он, чуть позже, меняет на 150 и тоже сохраняет. И что в итоге? Твоя сотка, блядь, испарилась в небытие! В базе лежит 150. Вот тебе и потерянное обновление — один как будто и не работал. Пиздец обидно.

  2. Грязное чтение (Dirty Read). Это когда ты, такой довольный, читаешь данные, которые другая транзакция ещё даже не сохранила толком. А она возьми да откатись! А ты уже на эти «грязные» данные заложился, построил логику, и теперь сидишь с ебалом, полным недоумения. Читал-то хуйню, которая в итоге не состоялась.

  3. Неповторяющееся чтение (Non-repeatable Read). Вообще анекдот. В рамках одной своей же транзакции ты дважды читаешь одну запись. А между этими чтениями какая-то тварь незаметно её изменила и закоммитила. И ты такой: «Стоп, а я вроде только что видел тут другое значение... Ёпта, я что, уже глючу?». Нет, просто данные под твоим носом поменялись.

  4. Фантомное чтение (Phantom Read). Ещё круче. Ты делаешь выборку, например, «дай мне всех пользователей из Москвы». Получаешь 10 штук. Проходит миллисекунда, ты делаешь ТОЧНО ТАКОЙ ЖЕ запрос, а тебе уже 11 или 9! Как будто фантом какой-то строки то появляется, то исчезает. На самом деле, просто параллельная транзакция кого-то добавила или удалила. Волнение ебать, не правда ли?

Живой пример, как всё просрать (тот самый Lost Update)

Смотри, как это выглядит в коде, если ничего не предусмотреть:

// Транзакция 1 (Это ты, решивший поднять цену на 100)
entityManager.getTransaction().begin();
Product p1 = entityManager.find(Product.class, 1L); // Читает price = 100
// А в этот самый момент, блядь, вклинивается Транзакция 2 (твой «кореш»):
// Product p2 = entityManager.find(Product.class, 1L);
// p2.setPrice(p2.getPrice() + 50); // price = 150
// entityManager.getTransaction().commit(); // Успел раньше, сука!
p1.setPrice(p1.getPrice() + 100); // Ты-то думаешь, что price ещё 100, и ставишь 200
entityManager.getTransaction().commit(); // И БАМ! Перезаписываешь своей двойкой его 150. Его полтинник — в тартарары.

Красота, да? Оба думали, что работают, а по факту труд одного пошёл нахуй.

Как со всем этим бороться, чтобы не сойти с ума

  • Уровни изоляции транзакций. Это как степени паранойи в системе. От «да похуй, читай что хочешь» (READ_UNCOMMITTED) до «все встали в строй и выполняются строго по очереди, блядь!» (SERIALIZABLE). Чем выше уровень, тем меньше аномалий, но тем всё медленнее и печальнее. Выбирай, что тебе дороже: скорость или целостность данных. Обычно READ_COMMITTED — золотая середина, грязные чтения отсекает.

  • Механизмы блокировок.

    • Пессимистичные. Это когда ты с порога заявляешь: «Эта запись — моя, все нахуй!». Вешаешь на неё замок (SELECT ... FOR UPDATE) и спокойно работаешь, пока остальные ждут. Надёжно, но если все начнут так делать, можно в дедлоке зависнуть.
    • Оптимистичные. А это по-русски «авось пронесёт». Ты работаешь с данными, а система тихонько следит за их версией (через поле version). Когда ты идёшь сохранять, она проверяет: «А не пиздит ли он? Не изменил ли кто данные пока он тут умничал?». Если изменил — тебе вежливо показывают ошибку: «Извини, чувак, но пока ты думал, мир изменился. Начни заново». Менее затратно, но требует обработки этих самых конфликтов.

Вот и вся магия. Главное — понимать, что в многопользовательской среде база данных это не тихий омут, а ебаный цирк с конями, и готовиться к этому соответственно.