Что такое фантомное чтение (Phantom Read)
Вновь возникшие строки, которых не было при первом чтении, называют «строками-фантомами».
🟢 Junior Level
Фантомное чтение (Phantom Read) — это аномалия параллельного доступа к данным, при которой одна транзакция дважды выполняет поиск по диапазону строк с одним и тем же предикатным условием (например, WHERE status = 'NEW'), но второй запрос возвращает другой набор строк (появились новые строки или исчезли старые), потому что параллельная транзакция успела вставить (INSERT) или удалить (DELETE) записи и зафиксировать коммит (COMMIT).
Вновь возникшие строки, которых не было при первом чтении, называют «строками-фантомами».
Пример сценария фантомного чтения
Время Транзакция А (Генерация отчета) Транзакция Б (Новый заказ клиента)
| ------------------------------- ----------------------------------
| T1: BEGIN;
| T2: SELECT COUNT(*) FROM orders -- Результат: 10 заказов
| WHERE status = 'PENDING';
| T3: BEGIN;
| T4: INSERT INTO orders (id, status, amount)
| VALUES (11, 'PENDING', 5000);
| COMMIT; -- Зафиксировано!
| T5: SELECT SUM(amount) FROM orders -- Считает сумму уже по 11 заказам!
| WHERE status = 'PENDING'; -- Итоговый отчет: 10 заказов на сумму 11 заказов!
v T6: COMMIT;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderAuditService {
// Защита от фантомов на уровне базы данных:
@Transactional(isolation = Isolation.REPEATABLE_READ, readOnly = true)
public AuditReport calculateDailySummary() {
long count = orderRepository.countByStatus("PENDING");
BigDecimal total = orderRepository.sumByStatus("PENDING");
// На уровне Repeatable Read в PostgreSQL/MySQL оба запроса
// увидят строго одинаковый набор строк.
return new AuditReport(count, total);
}
}
Аналогия: Экскурсовод пересчитывает туристов в автобусе: «Всего 20 человек». Пока он делает пометку в журнале, в заднюю дверь тихо заходят еще двое опоздавших. Экскурсовод считает снова — теперь туристов 22. Двое новых туристов — это фантомы.
🟡 Middle Level
Различие: Неповторяющееся чтение vs Фантомное чтение
| Критерий | Неповторяющееся чтение (Non-Repeatable Read) | Фантомное чтение (Phantom Read) |
|---|---|---|
| Что меняется | Значения в уже существующих строках | Количество строк в результирующей выборке |
| SQL-команда конкурента | UPDATE (или точечный DELETE) |
INSERT (или массовый DELETE по диапазону) |
| Тип предиката | Точечный поиск строки: WHERE id = 42 |
Предикатный диапазон: WHERE price > 100 |
| Как защититься | Строчные блокировки SELECT FOR UPDATE |
Блокировка диапазонов (Gap Locks) или MVCC Snapshot |
Поведение Repeatable Read в различных СУБД
По формальному стандарту ANSI SQL-92 уровень Repeatable Read обязан защищать от Dirty Read и Non-Repeatable Read, но имеет право допускать фантомное чтение:
| СУБД | Фантомы на Repeatable Read? |
Механизм защиты |
|---|---|---|
| PostgreSQL | ❌ Защищено (для чистых SELECT) | MVCC Snapshot с момента первого запроса |
| MySQL (InnoDB) | ❌ Защищено (для SELECT и FOR UPDATE) | Next-Key Locks (Record Lock + Gap Lock) |
| Oracle | ❌ Защищено | MVCC Snapshot (аналог PostgreSQL) |
| MS SQL Server | ⚠️ Возможны! | Ставит S-блокировки только на найденные строки, зазоры свободны |
🔴 Senior Level
Механика Next-Key Locking в MySQL (InnoDB)
В MySQL InnoDB на уровне Repeatable Read для предотвращения фантомов используется гибридный механизм:
-- Таблица с записями id = 5, 10, 15
SELECT * FROM orders WHERE id BETWEEN 8 AND 12 FOR UPDATE;
Что именно блокирует InnoDB:
- Record Lock: Блокирует саму запись
id = 10. - Gap Lock (слева): Блокирует интервал $(5, 10)$ — ни одна другая транзакция не может выполнить
INSERTсо значением $id \in [6, 9]$. - Gap Lock (справа): Блокирует интервал $(10, 15)$ — ни одна транзакция не может выполнить
INSERTсо значением $id \in [11, 14]$.
Побочный эффект Gap Locks: Блокировки зазоров не конфликтуют между собой на чтение, но конфликтуют со вставками. Если две параллельные транзакции накладывают пересекающиеся Gap-блокировки, последующие операторы INSERT обеих транзакций гарантированно приведут к взаимной блокировке (Deadlock).
Механика Predicate Locks в PostgreSQL (SSI)
PostgreSQL на уровне Repeatable Read не использует блокировки зазоров, полагаясь на неизменяемость MVCC-снимка.
Однако на уровне Serializable для полной изоляции задействуется алгоритм SSI (Serializable Snapshot Isolation):
- При предикатном запросе
SELECT * FROM orders WHERE status = 'PENDING'регистрируется Predicate Lock (предикатный замок в оперативной памяти). - Предикатный замок связывается со страницами индекса B-Tree, покрывающими условие
status = 'PENDING'. - Если параллельная транзакция вставляет новую запись, попадающую в эту же страницу индекса, ядро PostgreSQL мгновенно фиксирует возникновение rw-антизависимости ($T_{reader} \xrightarrow{rw} T_{inserter}$).
- При обнаружении циклов зависимостей одна из транзакций прерывается с ошибкой сериализации
40001.
4 Tricky Questions
1. Почему в стандарте ANSI SQL уровень Repeatable Read допускает появление фантомов, а в PostgreSQL они не воспроизводятся при обычных SELECT?
Ответ:
Стандарт ANSI SQL-92 создавался в эпоху доминирования классических блокировочников (Lock-Based Concurrency Control / 2PL).
В блокировочных СУБД для обеспечения Repeatable Read на прочитанные строки вешались разделяемые блокировки (S-locks). Поскольку блокировались только уже существующие строки, новая строка свободно вставлялась в таблицу параллельной транзакцией, порождая фантом.
В PostgreSQL изоляция реализована на базе MVCC. Снимок данных замораживает транзакционный горизонт (xmin/xmax). Любая новая строка, вставленная параллельной транзакцией, получает xmin, который больше горизонта текущего снимка, и аппаратно отсекается ядром СУБД при любых последующих SELECT.
2. Может ли SELECT ... FOR UPDATE на уровне Read Committed защитить от появления фантомов при вставке новых записей?
Ответ:
Нет, в общем случае не может!
Запрос SELECT ... FOR UPDATE на уровне Read Committed блокирует монопольными блокировками (X-locks) исключительно те строки, которые уже физически существуют в таблице на момент выполнения запроса.
Если параллельная транзакция выполняет INSERT новой строки, удовлетворяющей условию предиката:
- Новой строки в таблице еще не было, заблокировать ее было невозможно.
- На уровне
Read Committedблокировки зазоров (Gap Locks) не накладываются. - Вставка успешно выполнится и закоммитится.
Повторный запрос
SELECT ... FOR UPDATEна уровнеRead Committedувидит новую строку и наложит на нее замок, зафиксировав фантом. Предотвратить фантомы можно только на уровнеSerializableилиRepeatable Readв MySQL.
3. В чем заключается аномалия «Фантомного обновления» (Phantom Update) в PostgreSQL на уровне Repeatable Read?
Ответ:
Хотя обычный SELECT в PostgreSQL на Repeatable Read защищен от фантомов, операция UPDATE работает по текущим закоммиченным данным (Current Read).
Рассмотрим сценарий:
-- Транзакция T1 (Repeatable Read):
SELECT count(*) FROM orders WHERE status = 'PENDING'; -- Видит 5 строк
-- Транзакция T2:
INSERT INTO orders (status) VALUES ('PENDING'); -- Вставляет 6-ю строку!
COMMIT;
-- Транзакция T1 выполняет массовый UPDATE:
UPDATE orders SET status = 'PROCESSED' WHERE status = 'PENDING';
-- Результат: 6 rows updated! (T1 обновила строку, которую сама не видела!)
-- Повторный SELECT в T1:
SELECT count(*) FROM orders WHERE status = 'PROCESSED'; -- Вернет 6 строк!
Транзакция $T1$ обновила скрытую от нее строку-фантом, а согласно правилам MVCC транзакция всегда видит строки, модифицированные ею самой.
4. Почему использование временных таблиц (Temporary Tables) может нарушить защиту от фантомов на уровне Serializable в PostgreSQL?
Ответ:
В PostgreSQL алгоритм Serializable Snapshot Isolation (SSI) отслеживает предикатные блокировки (SIREAD locks) только для постоянных таблиц базы данных.
Временные таблицы (CREATE TEMP TABLE) сессионно изолированы и не регистрируются в общем менеджере предикатных блокировок shared memory. Если бизнес-логика копирует данные во временную таблицу, выполняет над ней агрегатные расчеты и принимает решения для основной базы, алгоритм SSI не сможет обнаружить межтранзакционные конфликты зависимостей (rw-antidependencies), что приведет к пропуску фантомов и нарушению сериализуемости.
🎯 Шпаргалка для интервью
- Суть Phantom Read: Повторный предикатный запрос (
WHERE condition) возвращает другое количество строк из-за параллельногоINSERTилиDELETEиCOMMIT. - Разница с Non-Repeatable Read:
- Non-Repeatable Read — меняются поля уже прочитанной строки (
UPDATE). - Phantom Read — меняется состав строк в диапазоне выборки (
INSERT/DELETE).
- Non-Repeatable Read — меняются поля уже прочитанной строки (
- Поведение в СУБД:
PostgreSQL: MVCC-снимок защищает от фантомов при обычныхSELECTна уровнеRepeatable Read.MySQL InnoDB: Next-Key Locks (Record + Gap Locks) защищают от фантомов наRepeatable Read.MS SQL Server: НаRepeatable Readфантомы возможны.
- Next-Key Lock: Комбинация блокировки записи и блокировки интервалов между индексами (предотвращает вставки в зазоры, но провоцирует Deadlocks).
- Абсолютная защита: Гарантируется только на уровне
Serializable.