Перейти к содержимому

Аномалии транзакций

Аномалии транзакций — проблемы при параллельном выполнении транзакций. Грязное чтение (Dirty Read) — чтение незафиксированных изменений другой транзакции (допускает READ UNCOMMITTED). Неповторяющееся чтение (Non-Repeatable Read) — повторное чтение той же строки даёт разные значения (READ UNCOMMITTED, READ COMMITTED). Фантомное чтение (Phantom Read) — появление новых строк при повторной выборке (READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ по стандарту; PostgreSQL и MySQL InnoDB защищают на REPEATABLE READ). Потерянное обновление (Lost Update) — изменение одной транзакции перезаписывается другой (требует явной блокировки SELECT FOR UPDATE). Грязная запись (Dirty Write) — запись поверх незафиксированных данных (запрещена в современных СУБД). Зависание записи (Write Skew) — обновление разных строк нарушает общее ограничение (возможно на REPEATABLE READ, защита — SERIALIZABLE или блокировка диапазона). Аномалия сериализации — результат не соответствует последовательному выполнению. Сравнение аномалий (допускающие уровни, защищающие уровни). Особенности в PostgreSQL, MySQL InnoDB, SQL Server, Oracle. Бизнес-последствия каждой аномалии.

Введение: Когда параллельность ломает логику

Представьте, что вы и ваш коллега одновременно работаете с одной и той же электронной таблицей. Вы видите, что в ячейке A1 написано “100”. Вы решаете увеличить это значение на 10 и начинаете вводить “110”. В тот же момент коллега видит “100” и решает уменьшить его на 20, начиная вводить “80”. Кто победит? Чей результат останется в ячейке?

В мире электронных таблиц эта проблема решается просто: последний сохранившийся результат перезаписывает предыдущий. Но в базах данных, где транзакции могут длиться доли секунды и выполняться тысячами одновременно, последствия “перезаписи” могут быть катастрофическими.

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

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

Классификация аномалий

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

Аномалии чтения (чтение данных в несогласованном состоянии):

АномалияКраткое описание
Грязное чтение (Dirty Read)Чтение незафиксированных изменений другой транзакции
Неповторяющееся чтение (Non-Repeatable Read)Повторное чтение той же строки дает разные значения
Фантомное чтение (Phantom Read)При повторной выборке появляются новые строки

Аномалии обновления (конфликты при изменении данных):

АномалияКраткое описание
Потерянное обновление (Lost Update)Изменения одной транзакции перезаписываются другой
Грязная запись (Dirty Write)Запись незафиксированных данных другой транзакции

Аномалии структуры и порядка (более тонкие проблемы):

АномалияКраткое описание
Фантом (Phantom)Появление новых строк, удовлетворяющих условию
Аномалия сериализацииНарушение порядка выполнения транзакций
Зависание записи (Write Skew)Связанное обновление разных строк на основе устаревших данных

В этом документе мы подробно разберем каждую из этих аномалий, покажем на примерах, как они возникают, и объясним, почему они опасны для бизнеса.

Грязное чтение (Dirty Read)

Что это такое

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

Название “грязное” подчеркивает, что эти данные — “грязные”, они еще не очищены фиксацией, они могут исчезнуть или измениться.

Почему это опасно

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

Бизнес-сценарий: отчет на основе незафиксированных данных

Представьте, что менеджер формирует отчет о текущих остатках на складе. В тот же момент кладовщик проводит инвентаризацию: временно обнуляет остатки проблемного товара, планируя потом вернуть их. Если отчет прочитает эти временные нули, менеджер примет решение о закупке товара, которого на самом деле достаточно. Деньги будут потрачены зря.

Пример

-- Транзакция A (кладовщик, проводит инвентаризацию)
BEGIN;
UPDATE products SET stock = 0 WHERE id = 1;  -- Временно обнулил остатки

-- В этот момент Транзакция B (менеджер, формирует отчет)
BEGIN;
SELECT stock FROM products WHERE id = 1;   -- Читает 0 (грязное чтение!)
-- Отчет показывает, что товар закончился

-- Транзакция A
ROLLBACK;  -- Откат: остатки вернулись к 100

-- Результат: отчет основан на данных, которых никогда не было

Как защититься

Грязное чтение допускает только уровень изоляции READ UNCOMMITTED. Все остальные уровни (READ COMMITTED и выше) защищают от этой аномалии.

Неповторяющееся чтение (Non-Repeatable Read)

Что это такое

Неповторяющееся чтение — это ситуация, когда транзакция дважды читает одну и ту же строку и получает разные значения. Между двумя чтениями другая транзакция изменила эту строку и зафиксировала изменения.

Название отражает суть: “неповторяющееся” — если повторить чтение, результат будет другим.

Отличие от грязного чтения

Ключевое отличие: при грязном чтении транзакция читает незафиксированные данные. При неповторяющемся чтении транзакция читает зафиксированные данные — просто между двумя чтениями данные успели измениться.

Почему это опасно

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

Бизнес-сценарий: анализ баланса счета

Представьте, что банковский служащий проверяет баланс счета клиента, чтобы одобрить кредит. Он видит 1 000 000 рублей. Пока он оформляет документы, клиент снимает 900 000 рублей в банкомате. Служащий повторно проверяет баланс перед одобрением — видит 100 000 рублей. Кредит не одобрен, хотя первая проверка показывала достаточный баланс. Клиент в недоумении.

Пример

-- Транзакция A (банковский служащий)
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- Получает 1 000 000

-- Транзакция B (клиент снимает деньги)
BEGIN;
UPDATE accounts SET balance = balance - 900000 WHERE id = 1;
COMMIT;  -- Баланс стал 100 000

-- Транзакция A (повторная проверка)
SELECT balance FROM accounts WHERE id = 1;  -- Получает 100 000 (неповторяющееся чтение!)
COMMIT;

-- Результат: противоречивая информация в рамках одной транзакции

Как защититься

Неповторяющееся чтение допускают уровни READ UNCOMMITTED и READ COMMITTED. Уровни REPEATABLE READ и SERIALIZABLE защищают от этой аномалии.

Фантомное чтение (Phantom Read)

Что это такое

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

Эти новые или исчезнувшие строки называются “фантомами” — они появляются как призраки между двумя чтениями.

Отличие от неповторяющегося чтения

При неповторяющемся чтении меняется значение существующей строки. При фантомном чтении меняется сам набор строк — появляются новые или исчезают старые.

Почему это опасно

Фантомы могут нарушать логику, которая зависит от количества строк или от полного набора данных.

Бизнес-сценарий: выдача уникальных слотов

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

Пример

-- Транзакция A (администратор, бронирует все свободные места)
BEGIN;
SELECT COUNT(*) FROM seats WHERE is_free = true;  -- Получает 10 свободных мест

-- Транзакция B (другой администратор, бронирует одно место)
BEGIN;
UPDATE seats SET is_free = false WHERE seat_id = 42 AND is_free = true;
COMMIT;  -- Теперь свободно 9 мест

-- Транзакция A (повторная проверка перед бронированием)
SELECT COUNT(*) FROM seats WHERE is_free = true;  -- Получает 9 (фантомное чтение!)
-- Ошибка: нельзя забронировать 10 мест, свободно только 9
COMMIT;

Как защититься

Фантомное чтение допускают уровни READ UNCOMMITTED, READ COMMITTED и REPEATABLE READ (по стандарту SQL). Уровень SERIALIZABLE защищает от фантомов. Однако некоторые СУБД (PostgreSQL, MySQL InnoDB) защищают от фантомов уже на уровне REPEATABLE READ.

Потерянное обновление (Lost Update)

Что это такое

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

Это классическая проблема “гонки” (race condition) в параллельном программировании.

Варианты потерянного обновления

Классическое потерянное обновление (без блокировок):

-- Транзакция A и B одновременно читают balance = 100
-- Транзакция A: balance = 100 + 10 = 110
-- Транзакция B: balance = 100 + 20 = 120
-- Транзакция A записывает 110
-- Транзакция B записывает 120
-- Результат: 120 (изменение A потеряно, хотя должно быть 130)

Потерянное обновление при проверке условия:

-- Две транзакции читают остаток товара (stock = 5)
-- Обе проверяют: stock > 0 (да)
-- Обе уменьшают stock на 1
-- Результат: stock = 4 (должно быть 3, продано 2 единицы)

Почему это опасно

Потерянное обновление может приводить к прямой потере денег, товаров или других ценных ресурсов.

Бизнес-сценарий: одновременная продажа последнего товара

Два покупателя одновременно нажимают “Купить” на последний товар в интернет-магазине. Оба видят остаток 1. Оба списывают 1. Если система не защищена от потерянных обновлений, оба получат подтверждение, но в базе данных остаток станет -1 (или 0, но один покупатель купил товар, которого не было). Товар продан дважды.

Как защититься

Потерянное обновление — это особый случай, потому что оно может возникать даже на уровне SERIALIZABLE, если транзакции выполняются определенным образом. Защита требует явной блокировки строки (SELECT FOR UPDATE) или оптимистичной блокировки с версионированием.

-- Защита через явную блокировку
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;  -- Блокируем строку
-- Теперь другие транзакции не могут изменить эту строку, пока мы не закончим
UPDATE accounts SET balance = balance + 10 WHERE id = 1;
COMMIT;

Грязная запись (Dirty Write)

Что это такое

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

Почему это особенно опасно

Грязная запись может привести к тому, что зафиксированная транзакция частично перезапишет данные, которые в итоге будут откачены. Это создает ситуации, которые невозможно разрешить даже при восстановлении после сбоя.

Большинство современных СУБД (PostgreSQL, MySQL InnoDB, Oracle, SQL Server) не допускают грязных записей даже на самом низком уровне изоляции. Это считается фундаментальным требованием к надежности.

Пример (в СУБД, которые допускают грязные записи, таких почти не осталось)

-- Транзакция A
BEGIN;
UPDATE products SET price = 100 WHERE id = 1;  -- Изменила цену на 100

-- Транзакция B (грязная запись)
BEGIN;
UPDATE products SET price = 200 WHERE id = 1;  -- Перезаписывает незафиксированные данные!

-- Транзакция A
ROLLBACK;  -- Что теперь? Цена должна вернуться к исходной? Но ее перезаписали...

Как защититься

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

Аномалия сериализации (Serialization Anomaly)

Что это такое

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

Другими словами, если транзакции выполняются параллельно, результат должен быть таким, как если бы они выполнялись одна за другой в каком-то порядке. Если это не так — налицо аномалия сериализации.

Почему это важно

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

Пример (более сложный, чем базовые аномалии)

-- Исходные данные: x = 10, y = 10

-- Транзакция A
BEGIN;
UPDATE accounts SET balance = balance + 5 WHERE id = 1;  -- x = 15
UPDATE accounts SET balance = balance - 5 WHERE id = 2;  -- y = 5
COMMIT;

-- Транзакция B
BEGIN;
SELECT SUM(balance) FROM accounts;  -- Читает x + y = 20
-- (в зависимости от момента может прочитать 15+10=25 или 10+5=15 или 20)
COMMIT;

В зависимости от того, когда транзакция B выполнит чтение, она может получить 25, 20 или 15. Результат 25 или 15 соответствует последовательному выполнению (сначала A потом B, или сначала B потом A). Результат 20 не соответствует ни одному последовательному порядку — это аномалия сериализации.

Как защититься

Только уровень SERIALIZABLE гарантирует отсутствие аномалий сериализации. Однако даже на SERIALIZABLE в современных СУБД с SSI (PostgreSQL) транзакции могут откатываться с ошибкой, если возникает потенциальная аномалия.

Зависание записи (Write Skew)

Что это такое

Зависание записи — это особая форма аномалии, которая возникает, когда две транзакции читают перекрывающиеся наборы данных, а затем обновляют разные, но связанные строки, нарушая бизнес-ограничение.

Это одна из самых коварных аномалий, потому что она может возникать даже на уровне REPEATABLE READ в некоторых СУБД.

Пример: Дежурство врачей

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

-- Исходные данные: doctors_on_call = 3

-- Транзакция A (Иванов уходит)
BEGIN;
SELECT COUNT(*) FROM doctors WHERE on_call = true;  -- Видит 3
-- Решает: можно уйти, останется 2
UPDATE doctors SET on_call = false WHERE name = 'Иванов';
COMMIT;

-- Транзакция B (Петров уходит) -- выполняется в то же время
BEGIN;
SELECT COUNT(*) FROM doctors WHERE on_call = true;  -- Тоже видит 3
-- Решает: можно уйти, останется 2
UPDATE doctors SET on_call = false WHERE name = 'Петров';
COMMIT;

-- Результат: на смене остался только Сидоров (1 врач). 
-- Ограничение "хотя бы 2 врача" нарушено!

Каждая транзакция по отдельности действовала корректно. Они читали разные строки (но один и тот же набор) и обновляли разные строки. В результате бизнес-правило нарушено.

Отличие от потерянного обновления

При потерянном обновлении транзакции обновляют одну и ту же строку. При зависании записи транзакции обновляют разные строки, но нарушают общее ограничение.

Как защититься

Зависание записи может возникнуть на уровне REPEATABLE READ. Для защиты нужен SERIALIZABLE или явная блокировка:

-- Защита через блокировку диапазона
BEGIN;
SELECT COUNT(*) FROM doctors WHERE on_call = true FOR UPDATE;
-- Блокировка не дает другим транзакциям изменять эти строки
UPDATE doctors SET on_call = false WHERE name = 'Иванов';
COMMIT;

Чтение фантомов и несуществующих строк

Чтение несуществующей строки (Read Skew с отсутствием)

Это ситуация, когда транзакция ожидает увидеть строку, но не видит ее, потому что другая транзакция ее удалила, или наоборот — видит строку, которая появилась после начала транзакции.

Чтение неподтвержденной вставки

Близкая к фантомному чтению аномалия, когда транзакция читает данные, вставленные другой незафиксированной транзакцией. Это частный случай грязного чтения для вставок.

Пример

-- Транзакция A
BEGIN;
INSERT INTO products (id, name) VALUES (100, 'Новый товар');

-- Транзакция B (READ UNCOMMITTED)
BEGIN;
SELECT * FROM products WHERE id = 100;  -- Видит вставленную, но еще не зафиксированную строку
COMMIT;

-- Транзакция A
ROLLBACK;  -- Строка никогда не существовала

-- Транзакция B использовала несуществующие данные

Сравнительная таблица аномалий

АномалияКраткое описаниеДопускают уровниЗащищают уровни
Грязная записьЗапись поверх незафиксированных данных(почти никто)Все современные СУБД
Грязное чтениеЧтение незафиксированных данныхREAD UNCOMMITTEDREAD COMMITTED+
Потерянное обновлениеИзменение теряется при одновременной записиREAD COMMITTED (без защиты)Явная блокировка или SERIALIZABLE
Неповторяющееся чтениеДва чтения строки дают разные значенияREAD UNCOMMITTED, READ COMMITTEDREPEATABLE READ, SERIALIZABLE
Фантомное чтениеПоявляются новые строки при повторной выборкеREAD UNCOMMITTED, READ COMMITTED, REPEATABLE READ*SERIALIZABLE
Зависание записиОбновление связанных строк нарушает ограничениеREPEATABLE READ (некоторые СУБД)SERIALIZABLE
Аномалия сериализацииРезультат не соответствует последовательному выполнениюВсе, кроме SERIALIZABLESERIALIZABLE

* Некоторые СУБД (PostgreSQL, MySQL InnoDB) защищают от фантомов уже на REPEATABLE READ, хотя стандарт этого не требует.

Аномалии в разных СУБД: Нюансы реализации

Один и тот же уровень изоляции может вести себя по-разному в разных СУБД. Это особенно заметно на примере REPEATABLE READ.

PostgreSQL

УровеньЗащита от фантомовЗащита от зависания записи
READ COMMITTEDНетНет
REPEATABLE READДаНет (зависание возможно)
SERIALIZABLEДаДа (с возможным откатом)

MySQL InnoDB

УровеньЗащита от фантомовЗащита от зависания записи
READ COMMITTEDНетНет
REPEATABLE READДа (через gap-блокировки)Нет
SERIALIZABLEДаДа (блокировками)

SQL Server

УровеньЗащита от фантомовЗащита от зависания записи
READ COMMITTEDНетНет
REPEATABLE READНетНет
SERIALIZABLEДаДа

Oracle

УровеньЗащита от фантомовЗащита от зависания записи
READ COMMITTEDНетНет
SERIALIZABLEДаДа

Как аномалии связаны с бизнес-требованиями

Понимание аномалий нужно не ради абстрактной “правильности”, а потому что каждая аномалия может привести к реальным бизнес-проблемам.

АномалияБизнес-последствие
Грязное чтениеРешения на основе данных, которые могут исчезнуть
Неповторяющееся чтениеПротиворечивая информация в рамках одной операции
Фантомное чтениеНеверный подсчет количества, двойное бронирование
Потерянное обновлениеПрямая потеря денег или товаров
Зависание записиНарушение бизнес-ограничений (например, дежурств, лимитов)

Практический подход: Для каждого сценария работы с данными определите, какие аномалии допустимы, а какие — нет. Это и будет вашим требованием к уровню изоляции.

Резюме для системного аналитика

  1. Аномалии транзакций — это проблемы, возникающие при параллельном выполнении. Они могут приводить к неверным данным, потере обновлений и нарушению бизнес-правил.

  2. Грязное чтение — самая опасная аномалия. Транзакция читает данные, которые могут быть откачены. Защита: уровень изоляции не ниже READ COMMITTED.

  3. Неповторяющееся чтение — изменение значения строки между двумя чтениями. Защита: REPEATABLE READ и выше.

  4. Фантомное чтение — появление новых строк между двумя выборками. Защита: SERIALIZABLE (или REPEATABLE READ в некоторых СУБД).

  5. Потерянное обновление — изменения одной транзакции перезаписываются другой. Требует явной блокировки или особого внимания.

  6. Зависание записи — коварная аномалия, при которой транзакции обновляют разные строки, но нарушают общее ограничение. Защита: SERIALIZABLE или явная блокировка диапазона.

  7. Одна и та же СУБД может реализовывать защиту от аномалий по-разному. PostgreSQL защищает от фантомов на REPEATABLE READ, SQL Server — нет. Знайте особенности вашей СУБД.

Проверка знаний

Вопрос 1 из 4
Что такое аномалии транзакций?
Какой пример соответствует dirty read?
Какой пример соответствует non-repeatable read?
Почему важно знать аномалии транзакций?

Вопросы, где были ошибки