Какие уровни изоляции транзакций существуют
Уровни изоляции транзакций определяют степень изоляции (защиты) данных одной транзакции от одновременных изменений, производимых другими параллельными транзакциями.
🟢 Junior Level
Уровни изоляции транзакций определяют степень изоляции (защиты) данных одной транзакции от одновременных изменений, производимых другими параллельными транзакциями.
Стандарт ANSI/ISO SQL-92 определяет 4 классических уровня изоляции, упорядоченных по возрастанию строгости и снижению производительности:
- Read Uncommitted (Чтение незафиксированных данных): Самый низкий уровень. Транзакция может читать изменения, сделанные другими транзакциями, которые еще не были зафиксированы (
COMMIT). - Read Committed (Чтение зафиксированных данных): Транзакция видит только те данные, которые были успешно закоммичены до момента чтения. Защищает от «грязного» чтения.
- Repeatable Read (Повторяющееся чтение): Гарантирует, что если транзакция прочитала строку данных, то при повторном чтении этой же строки внутри той же транзакции она увидит в точности те же самые данные.
- Serializable (Сериализуемый): Самый строгий уровень. Транзакции выполняются с гарантией полного отсутствия аномалий параллелизма, как если бы они выполнялись строго последовательно одна за другой.
Сводная таблица классических аномалий
| Уровень изоляции | Грязное чтение (Dirty Read) | Неповторяющееся чтение (Non-Repeatable Read) | Фантомное чтение (Phantom Read) |
|---|---|---|---|
| Read Uncommitted | ⚠️ Допускается | ⚠️ Допускается | ⚠️ Допускается |
| Read Committed | ❌ Защищено | ⚠️ Допускается | ⚠️ Допускается |
| Repeatable Read | ❌ Защищено | ❌ Защищено | ⚠️ Допускается (кроме MySQL InnoDB*) |
| Serializable | ❌ Защищено | ❌ Защищено | ❌ Защищено |
*В MySQL (InnoDB) механизм предикатных блокировок Next-Key Locking предотвращает фантомы даже на уровне Repeatable Read.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
// Явное задание уровня изоляции в Spring
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void generateMonthlyReport(Long accountId) {
// Повторные SELECT по accountId гарантированно вернут одинаковые строки
}
}
Аналогия: Уровни изоляции напоминают перегородки в читальном зале: от полного отсутствия стен, где все читают через плечо соседа (Read Uncommitted), до легких шторок (Read Committed), отдельных кабинок (Repeatable Read) и отдельной запертой комнаты с охраной для одного читателя (Serializable).
🟡 Middle Level
Описание аномалий параллельного доступа
- Грязное чтение (Dirty Read):
Транзакция $T_1$ изменила строку, но еще не зафиксировала коммит. Транзакция $T_2$ прочитала эту измененную строку. Затем $T_1$ выполняет
ROLLBACK. Транзакция $T_2$ осталась с данными, которых физически не существует. - Неповторяющееся чтение (Non-Repeatable / Fuzzy Read):
Транзакция $T_1$ считывает строку со значением
balance = 100. Транзакция $T_2$ обновляет баланс на150и делаетCOMMIT. Транзакция $T_1$ повторно читает ту же строку и получает уже150. - Фантомное чтение (Phantom Read):
Транзакция $T_1$ запрашивает диапазон строк:
SELECT * FROM users WHERE age > 18(получает 5 записей). Транзакция $T_2$ вставляет нового пользователя сage = 20и делаетCOMMIT. Транзакция $T_1$ повторяет тот же запрос и получает уже 6 строк (появилась строка-фантом).
Дефолтные уровни изоляции в популярных СУБД
- PostgreSQL, Oracle, Microsoft SQL Server: По умолчанию используют Read Committed. Это обеспечивает максимальную пропускную способность (TPS) и исключает накладные расходы на длительное хранение старых версий строк.
- MySQL (InnoDB): Исторически использует по умолчанию Repeatable Read (в ранних версиях MySQL это требовалось для корректной работы репликации на основе SQL-выражений Statement-Based Replication).
Архитектурное поведение уровней в PostgreSQL
В PostgreSQL механизм MVCC устроен так, что создание снимка данных (Snapshot) является естественной операцией:
- На уровне Read Committed: Свежий снимок данных (Snapshot) создается перед выполнением каждого отдельного SQL-запроса внутри транзакции.
- На уровне Repeatable Read: Снимок данных создается ровно один раз в момент старта первого запроса транзакции и остается неизменным до вызова
COMMIT.
🔴 Senior Level
Аномалия Write Skew (Перекос записи) и Snapshot Isolation
Классический стандарт ANSI SQL-92 критикуют за неполноту, так как он не учитывал многоверсионность (MVCC) и пропустил критическую аномалию — Write Skew.
Многие промышленные СУБД (PostgreSQL, Oracle) под уровнем Repeatable Read фактически реализуют алгоритм Snapshot Isolation (Изоляция снимка).
Snapshot Isolation защищает от Dirty Read, Non-Repeatable Read и фантомов при чтении, но допускает аномалию Write Skew.
Пример аномалии Write Skew:
- Бизнес-правило: В дежурстве больницы должен оставаться хотя бы один врач (
count(on_call) >= 1). Сейчас дежурят двое: Доктор А и Доктор Б. - Транзакция 1 (Доктор А): Проверяет
SELECT count(*) WHERE on_call = true(результат: 2). Снимает дежурство с Доктора А:UPDATE doctors SET on_call = false WHERE name = 'A'. - Транзакция 2 (Доктор Б): Одновременно проверяет
SELECT count(*)(результат: 2, так как $T_1$ еще не закоммичена). Снимает дежурство с Доктора Б:UPDATE doctors SET on_call = false WHERE name = 'B'. - Обе транзакции успешно коммитятся!
- Итог: На дежурстве осталось 0 врачей. Инвариант системы нарушен!
Ни одна строка не подверглась конфликту
UPDATEна одну и ту же запись, поэтомуRepeatable Read(Snapshot Isolation) успешно пропустил обе транзакции. Защитить от Write Skew может только уровеньSerializable(или явныйSELECT FOR UPDATE).
Serializable Snapshot Isolation (SSI) в PostgreSQL
Вместо традиционных тяжелых пессимистических блокировок предикатов (Predicate Locks, используемых в 2PL), PostgreSQL начиная с версии 9.1 использует передовой алгоритм SSI (Serializable Snapshot Isolation):
[T1: Читает A] ------ rw-antidependency -----> [T2: Пишет в A]
|
rw-antidependency
v
[T3: Читает B]
- Транзакции выполняются без блокировок чтения на базе снапшотов.
- В оперативной памяти поддерживаются неблокирующие легковесные замки
SIREAD locks. - Система отслеживает опасные структуры — циклы зависимостей вида «чтение-запись» (rw-antidependencies).
- При обнаружении потенциального цикла одна из транзакций прерывается ядром СУБД с ошибкой:
ERROR: could not serialize access due to read/write dependencies among transactions (SQLSTATE 40001).
Паттерн безопасного применения Serializable в Java / Spring
При работе на уровне Serializable приложение обязано иметь механизм повтора (Retry Logic), так как ошибки сериализации являются штатным событием:
import org.springframework.dao.CannotSerializeTransactionException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class HospitalService {
@Retryable(
retryFor = { CannotSerializeTransactionException.class },
maxAttempts = 5,
backoff = @Backoff(delay = 100, multiplier = 2.0)
)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void leaveOnCallDuty(String doctorName) {
long activeDoctors = doctorRepository.countByOnCallTrue();
if (activeDoctors <= 1) {
throw new IllegalStateException("Нельзя снять последнего дежурного врача!");
}
doctorRepository.updateOnCall(doctorName, false);
}
}
4 Tricky Questions
1. Почему в PostgreSQL установка SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; не приводит к грязному чтению?
Ответ:
В PostgreSQL уровень Read Uncommitted физически отсутствует в движке.
Если разработчик выполняет команду SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED, PostgreSQL парсит эту команду для обеспечения совместимости со стандартом ANSI SQL, но молча мапит ее на уровень Read Committed.
Архитектура PostgreSQL MVCC проверяет видимость каждого кортежа по статусу транзакции в журнале pg_xact. Кортеж, созданный незакоммиченной транзакцией, считается невидимым «из коробки» без каких-либо накладных расходов. Движок PostgreSQL технически не умеет отдавать «грязные» кортежи.
2. Почему в MySQL InnoDB уровень Repeatable Read защищает от фантомных чтений, а в PostgreSQL — нет?
Ответ: Разница заключается в механизмах изоляции:
- PostgreSQL: Реализует
Repeatable Readна чистом MVCC (Snapshot Isolation). Снимок видит только записи, закоммиченные до старта транзакции. Однако при выполнении последующих операций модификации (UPDATE ... WHERE) Postgres может затронуть строки, вставленные параллельной транзакцией, что приводит к аномалиям фантомов при записи. - MySQL (InnoDB): Для предотвращения фантомов дополняет MVCC специальным типом пессимистических блокировок — Next-Key Locks (комбинация блокировки самой записи
Record Lockи блокировки диапазона между записямиGap Lock). Если транзакция считывает диапазон строк с блокировкой, другие транзакции физически не могут вставить новую строку в этот диапазон.
3. Чем отличается создание снимка данных (Snapshot) в PostgreSQL на уровнях Read Committed и Repeatable Read?
Ответ:
- На уровне Read Committed: Снимок видимости кортежей формируется заново в начале каждого отдельного SQL-выражения (
statement-level snapshot). Если транзакция выполняетSELECT, затем ждет 5 секунд и повторяетSELECT, второй запрос получит новый снимок и увидит все коммиты, произошедшие за эти 5 секунд. - На уровне Repeatable Read: Снимок видимости создается один раз в момент выполнения первого запроса транзакции (
transaction-level snapshot) и сохраняется до конца транзакции. Любые повторные запросы в рамках транзакции видят базу данных в строго зафиксированном состоянии на момент старта.
4. В чем заключается опасность длительных транзакций на уровне Repeatable Read и Serializable для СУБД?
Ответ: Длительные транзакции с долгоживущими снимками данных блокируют процесс очистки старых версий строк:
- В PostgreSQL (Table Bloat): Фоновый процесс
VACUUMне имеет права удалять мертвые строки (dead tuples), пока существует хотя бы одна транзакция, чейxminстарше транзакции, удалившей эту строку. Длительная транзакция «замораживает» вакуум, приводя к лавинообразному распуханию таблиц и индексов на диске. - В MySQL InnoDB (Undo Log Overflow): Сервер вынужден хранить цепочки отката в сегментах
Undo Tablespace. Длинная транзакция приводит к гигантскому росту системных файлов undo-логов и замедлению сборки мусора (Purge Thread).
🎯 Шпаргалка для интервью
- 4 уровня изоляции:
Read Uncommitted: Допускает Dirty, Non-repeatable, Phantom Read. В Postgres работает как Read Committed.Read Committed: Защищает от Dirty Read. Снимок создается на каждый SQL-запрос. Дефолт в Postgres, Oracle.Repeatable Read: Защищает от Dirty и Non-Repeatable Read. Снимок создается 1 раз на транзакцию. Дефолт в MySQL.Serializable: Полная изоляция (защита от фантомов и Write Skew).
- Аномалия Write Skew: Ситуация, когда две транзакции параллельно читают пересекающиеся данные, меняют непересекающиеся строки и нарушают общий инвариант. Решается только на
Serializableили черезSELECT FOR UPDATE. - PostgreSQL SSI: Не использует блокировки чтения; строит граф
rw-antidependenciesи отменяет конфликтующие транзакции (ошибка40001). - Retry Logic: На уровне
Serializableприложение обязательно должно иметь@Retryableна случайCannotSerializeTransactionException.