JavaRush /Курсы /SQL SELF /Аномалии при выполнении транзакций: Dirty Read

Аномалии при выполнении транзакций: Dirty Read, Non-Repeatable Read, Phantom Read

SQL SELF
40 уровень , 3 лекция
Открыта

Давайте еще раз пройдемся по типам транзакций! В мире транзакций в базах данных есть три главных "злодея", которые могут испортить вам день: Dirty Read, Non-Repeatable Read и Phantom Read. Эти "аномалии" появляются из-за недостаточного уровня изоляции транзакций. Сегодня разберем, кто эти злодеи, как они проявляются, и — что самое важное — как с ними бороться.

Прежде чем перейти к примерам, давайте вспомним, что они значат.

  • Dirty Read (Грязное чтение):
    Вы читаете данные, которые изменены, но транзакция, изменившая их, еще не зафиксирована (COMMIT) или, что хуже, может быть откатана (ROLLBACK). Это как если бы вы отправили деньги другу, посмотрели на ваш остаток и увидели, что вы разорены, но потом передумали и вернули деньги себе. Магия!

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

  • Phantom Read (Фантомное чтение):
    Вы делаете один и тот же запрос дважды, но во второй раз видите дополнительные строки, добавленные другой транзакцией. Это как если бы вы пересчитывали людей в комнате, а кто-то неприметно приводил еще друзей.

Проблема Dirty Read

Допустим, у нас есть таблица accounts:

CREATE TABLE accounts (
    account_id SERIAL PRIMARY KEY,
    owner TEXT NOT NULL,
    balance NUMERIC(10, 2) NOT NULL
);
INSERT INTO accounts (owner, balance) VALUES ('Alice', 1000), ('Bob', 500);

Транзакция 1 изменяет баланс, но еще не завершилась, а Транзакция 2 пытается одновременно прочитать эти данные.

Транзакция 1:

BEGIN;
UPDATE accounts SET balance = balance - 200 WHERE owner = 'Alice';
-- Баланс Alice стал 800, но транзакция еще не завершена.

Транзакция 2:

BEGIN;
SELECT balance FROM accounts WHERE owner = 'Alice'; -- Видим баланс: 800 (грязное чтение).
ROLLBACK; -- Транзакция 1 откатывается.

Теперь Транзакция 2 оперирует некорректными данными, потому что Транзакция 1 отменила изменения. Этого можно избежать, если использовать уровень изоляции READ COMMITTED, который не позволит видеть изменения незафиксированных транзакций.

Проблема Non-Repeatable Read

Представьте, что Транзакция 1 читает данные, другая транзакция их изменяет, и Транзакция 1 снова читает данные. Это данные отличаются.

Транзакция 1:

BEGIN;
SELECT balance FROM accounts WHERE owner = 'Bob'; -- Видим баланс: 500.

Транзакция 2 параллельно:

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Bob';
COMMIT;

Транзакция 1 снова:

SELECT balance FROM accounts WHERE owner = 'Bob'; -- Видим баланс: 400.
COMMIT;

Заметьте, как данные в рамках одной транзакции изменились. В реальных сценариях это может быть критично, например, для финансовых отчетов. Решается проблема использованием более высокого уровня изоляции, например, REPEATABLE READ.

Проблема Phantom Read

Допустим, у нас есть таблица orders:

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer TEXT NOT NULL,
    total NUMERIC(10, 2) NOT NULL
);
INSERT INTO orders (customer, total) VALUES ('Alice', 100), ('Bob', 200);

Транзакция 1 считает количество заказов, другая транзакция добавляет новый заказ, и Транзакция 1 снова считает.

Транзакция 1:

BEGIN;
SELECT COUNT(*) FROM orders; -- Видим: 2.

Транзакция 2 параллельно:

BEGIN;
INSERT INTO orders (customer, total) VALUES ('Charlie', 300);
COMMIT;

Транзакция 1 снова:

SELECT COUNT(*) FROM orders; -- Видим: 3. Новый "фантомный" заказ появился!
COMMIT;

Для устранения фантомных чтений потребуется уровень изоляции SERIALIZABLE, который полностью блокирует параллельные изменения, влияющие на результат.

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

Уровни изоляции vs. Аномалии

Уровень изоляции Dirty Read Non-Repeatable Read Phantom Read
Read Uncommitted ❌ Да ❌ Да ❌ Да
Read Committed ✅ Нет ❌ Да ❌ Да
Repeatable Read ✅ Нет ✅ Нет ❌ Да
Serializable ✅ Нет ✅ Нет ✅ Нет

Как выбрать правильный уровень изоляции?

  1. Если нужен быстрый доступ к данным и аномалии не критичны (например, аналитика на устаревших данных) — используйте READ COMMITTED.
  2. Если важна стабильность данных в пределах одной транзакции — используйте REPEATABLE READ.
  3. Если нужна максимальная изоляция и согласованность — выбирайте SERIALIZABLE. Помните: производительность может пострадать.

Практические рекомендации

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

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT ...;
COMMIT;

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

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

Специфические ошибки и как их избежать

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

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

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ