.NET Tutorial
Middle7 мин чтенияВопрос 3 из 17

Уровни изоляции транзакций: Read Uncommitted → Serializable, аномалии?

Аномалии

T2: отчётbalance = 1000T1: переводотчёт построен по данным,которых никогда не существовалоUPDATE balance = 0SELECT balance → 0 (dirty!)ROLLBACK
T2: отчётbalance = 1000T1: переводотчёт построен по данным,которых никогда не существовалоUPDATE balance = 0SELECT balance → 0 (dirty!)ROLLBACK
Dirty read — чтение незафиксированных данных
T2price = 100T1одно и то жечтение — разныезначенияSELECT price → 100UPDATE price = 120;COMMITSELECT price → 120
T2price = 100T1одно и то жечтение — разныезначенияSELECT price → 100UPDATE price = 120;COMMITSELECT price → 120
Non-repeatable read — строка изменилась между чтениями
T2ordersT1SELECT COUNT(*) WHEREstatus = 'new' → 5INSERT order status ='new'; COMMITSELECT COUNT(*) WHEREstatus = 'new' → 6
T2ordersT1SELECT COUNT(*) WHEREstatus = 'new' → 5INSERT order status ='new'; COMMITSELECT COUNT(*) WHEREstatus = 'new' → 6
Phantom read — появились новые строки
Врач Бдежурят: А и БВрач Аправило: дежуритьдолжен хотя быодиндежурных 0 — инвариант нарушен, хотя строкиразные и конфликта записи не былоSELECT count(дежурных) →2, можно уйтиSELECT count(дежурных) →2, можно уйтиUPDATE А недежурит; COMMITUPDATE Б недежурит; COMMIT
Врач Бдежурят: А и БВрач Аправило: дежуритьдолжен хотя быодиндежурных 0 — инвариант нарушен, хотя строкиразные и конфликта записи не былоSELECT count(дежурных) →2, можно уйтиSELECT count(дежурных) →2, можно уйтиUPDATE А недежурит; COMMITUPDATE Б недежурит; COMMIT
Write skew — правило нарушено двумя корректными транзакциями

Уровни и допускаемые аномалии

Уровень Dirty read Non-repeatable read Phantom read Lost update Write skew
Read Uncommitted Возможно Возможно Возможно Возможно Возможно
Read Committed Нет Возможно Возможно Возможно Возможно
Repeatable Read (стандарт) Нет Нет Возможно Нет* Возможно
Snapshot (PG Repeatable Read, SQL Server SNAPSHOT) Нет Нет Нет Нет (ошибка при конфликте) Возможно
Serializable Нет Нет Нет Нет Нет

* зависит от реализации.

Read Uncommitted

Read Committed
(по умолчанию в PG и MSSQL)

Repeatable Read /
Snapshot

Serializable

больше параллелизма, меньше гарантий ←→ меньше аномалий, больше блокировок и откатов

Read Uncommitted

Read Committed
(по умолчанию в PG и MSSQL)

Repeatable Read /
Snapshot

Serializable

больше параллелизма, меньше гарантий ←→ меньше аномалий, больше блокировок и откатов

Уровни изоляции как лестница

Как реализовано в популярных СУБД

PostgreSQL (MVCC)

  • Read Committed — каждый оператор видит снимок на момент своего начала.
  • Repeatable Read — вся транзакция видит снимок на момент первого запроса. Фантомов нет. Если пытаешься изменить строку, изменённую другой зафиксированной транзакцией, — ошибка could not serialize access due to concurrent update.
  • Serializable — Serializable Snapshot Isolation (SSI): отслеживает зависимости чтения/записи и откатывает транзакции, которые могли бы дать несериализуемый результат (в том числе write skew). Без блокировок чтения, но с ошибками 40001.
  • Read Uncommitted ведёт себя как Read Committed — грязного чтения нет вообще.

SQL Server

  • По умолчанию Read Committed на блокировках: читатели ставят разделяемые блокировки, писатели — эксклюзивные → чтение ждёт незавершённую запись.
  • READ_COMMITTED_SNAPSHOT (RCSI) — Read Committed на версиях строк (tempdb): читатели не блокируются. Включено по умолчанию в Azure SQL, в on-premise рекомендуется включать.
  • SNAPSHOT — отдельный уровень, аналог Repeatable Read в PostgreSQL.
  • Serializable — диапазонные блокировки (key-range locks): блокирует и «пустоты», чтобы не появились фантомы. Больше блокировок и deadlock-ов.
  • WITH (NOLOCK) = Read Uncommitted для таблицы — может вернуть незафиксированные, дублированные и пропущенные строки. Не используйте «для скорости».

Задание уровня

SQL
-- PostgreSQL
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- ...
COMMIT;

-- SQL Server
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRAN;
-- ...
COMMIT;
EF Core с повтором при ошибке сериализации
var strategy = db.Database.CreateExecutionStrategy();       // Npgsql: EnableRetryOnFailure повторяет 40001
await strategy.ExecuteAsync(async () =>
{
    db.ChangeTracker.Clear();
    await using var tx = await db.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct);

    var onCall = await db.Doctors.CountAsync(d => d.ShiftId == shiftId && d.OnCall, ct);
    if (onCall < 2) throw new BusinessRuleException("Нельзя уйти: останется меньше одного дежурного");

    var me = await db.Doctors.SingleAsync(d => d.Id == doctorId, ct);
    me.OnCall = false;
    await db.SaveChangesAsync(ct);
    await tx.CommitAsync(ct);
});

Как выбирать

Обычные CRUD-операции

Read Committed
+ optimistic concurrency (rowversion)

Отчёт / экспорт, которому нужен
согласованный снимок нескольких запросов

Repeatable Read / Snapshot
(read-only транзакция)

Инвариант охватывает несколько строк
(лимиты, расписания, дежурства)

Serializable + повтор,
или явная блокировка (FOR UPDATE),
или ограничение БД

«Для скорости» NOLOCK

Не использовать

Обычные CRUD-операции

Read Committed
+ optimistic concurrency (rowversion)

Отчёт / экспорт, которому нужен
согласованный снимок нескольких запросов

Repeatable Read / Snapshot
(read-only транзакция)

Инвариант охватывает несколько строк
(лимиты, расписания, дежурства)

Serializable + повтор,
или явная блокировка (FOR UPDATE),
или ограничение БД

«Для скорости» NOLOCK

Не использовать

Выбор уровня изоляции
  • Большинство приложений живут на Read Committed + optimistic concurrency для сущностей.
  • Для инвариантов на несколько строк — Serializable с повторами, или материализация конфликта: блокировка «родительской» строки (SELECT ... FROM shifts WHERE id = @id FOR UPDATE), уникальный индекс/исключающее ограничение (EXCLUDE в PostgreSQL для пересечений интервалов).

Вопросы на засыпку

Почему в PostgreSQL нельзя получить dirty read даже на Read Uncommitted?

Архитектура MVCC: транзакция видит только версии строк, созданные зафиксированными транзакциями (и свои). Показать чужую незафиксированную версию механизм не умеет, поэтому уровень просто отображается на Read Committed — стандарт это разрешает (он задаёт минимальные гарантии).

Чем snapshot isolation отличается от serializable?

Snapshot гарантирует, что транзакция видит согласованный снимок и не затирает чужие изменения тех же строк. Но две транзакции, читающие пересекающиеся данные и пишущие разные строки, могут вместе нарушить инвариант (write skew). Serializable это отлавливает.

Что такое ошибка сериализации и что с ней делать?

SQLSTATE 40001: СУБД не смогла выполнить транзакцию так, чтобы результат был эквивалентен последовательному выполнению, и откатила её. Это нормальная часть работы на высоких уровнях изоляции — транзакцию нужно повторить целиком (чтение + логика + запись).

Влияет ли уровень изоляции на read-only запросы?

Да: на Read Committed отчёт из нескольких запросов может увидеть данные из разных моментов (сумма по счетам не сойдётся). Для согласованных отчётов — одна транзакция Repeatable Read/Snapshot (BEGIN READ ONLY в PostgreSQL) или запрос к реплике.

Итог

Уровни изоляции — компромисс между корректностью и параллелизмом. Знайте аномалии (dirty, non-repeatable, phantom, lost update, write skew), знайте уровень по умолчанию своей СУБД и то, как он реализован (блокировки или MVCC). Большинству задач хватает Read Committed с optimistic concurrency, а многострочные инварианты требуют Serializable с повторами или явных блокировок.