← Назад к вопросам
Что такое фантомное чтение?
2.7 Senior🔥 151 комментариев
#Базы данных и SQL#Многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое фантомное чтение?
Фантомное чтение (Phantom Read) — это проблема в БД, когда одна транзакция видит строки, которые были добавлены другой транзакцией, нарушая консистентность данных.
Определение
Фантомное чтение происходит когда:
- Транзакция A выполняет SELECT с WHERE условием
- Транзакция B вставляет (INSERT) или удаляет (DELETE) строки, которые соответствуют условию A
- Транзакция A выполняет тот же SELECT еще раз
- Результаты отличаются! Появились новые строки (фантомы)
Пример
Исходное состояние таблицы users:
id | name | salary
---|---------|--------
1 | Alice | 5000
2 | Bob | 6000
Транзакция A (T1):
BEGIN TRANSACTION;
-- Запрос 1: найти всех сотрудников с зарплатой > 5500
SELECT * FROM users WHERE salary > 5500;
-- Результат: Bob (одна строка)
-- ... какая-то логика ...
Thread.sleep(2000); // Ждем 2 секунды
-- Запрос 2: снова ищем всех с зарплатой > 5500
SELECT * FROM users WHERE salary > 5500;
-- Результат: Bob, Charlie (ДВЕ строки!) - ФАНТОМНОЕ ЧТЕНИЕ
COMMIT;
Транзакция B (T2) - выполняется параллельно:
BEGIN TRANSACTION;
-- Транзакция A сделала первый SELECT
-- В промежутке T1 вставляет новую строку
INSERT INTO users VALUES (3, 'Charlie', 7000);
COMMIT;
-- Теперь в БД есть Charlie
Визуализация
Время Транзакция A (T1) Транзакция B (T2)
────────────────────────────────────────────────────────
t1 BEGIN
t2 SELECT (salary > 5500)
→ Bob (1 строка)
BEGIN
t3 INSERT Charlie (salary=7000)
COMMIT
t4 (delay)
t5 SELECT (salary > 5500)
→ Bob, Charlie (2 строки) ← ФАНТОМНОЕ ЧТЕНИЕ!
Результат изменился!
t6 COMMIT
Почему это проблема?
// Пример: расчет бонуса для сотрудников с зарплатой > 5500
public void calculateBonus(int minSalary) {
// Запрос 1
List<Employee> employees = db.query("SELECT * FROM users WHERE salary > " + minSalary);
int count = employees.size(); // count = 1 (только Bob)
double budget = 10000;
double bonusPerPerson = budget / count; // 10000 / 1 = 10000
// Логика обработки...
Thread.sleep(2000);
// Запрос 2
List<Employee> employees2 = db.query("SELECT * FROM users WHERE salary > " + minSalary);
int newCount = employees2.size(); // newCount = 2 (Bob, Charlie)!
// Проблема: мы рассчитали бонус для 1 сотрудника (10000)
// Но теперь нужно выплатить 2 сотрудникам
// Бюджет не хватит!
}
Чем отличается от других аномалий?
Dirty Read:
T1: SELECT * FROM users WHERE salary > 5500 → Bob
T2: INSERT Charlie (salary=7000) -- T2 НЕ COMMITTED
T1: SELECT * WHERE salary > 5500 → Bob, Charlie (из невыполненной T2!)
T2: ROLLBACK -- T2 откатилась!
Проблема: видим несвязанные данные
Non-repeatable Read:
T1: SELECT name FROM users WHERE id=1 → 'Alice'
T2: UPDATE users SET name='Alicia' WHERE id=1
T2: COMMIT
T1: SELECT name FROM users WHERE id=1 → 'Alicia' (тот же ID, но значение изменилось)
Проблема: один и тот же ID имеет разные значения
Phantom Read:
T1: SELECT * FROM users WHERE salary > 5500 → Bob
T2: INSERT Charlie (salary=7000) -- новая строка в диапазоне T1
T2: COMMIT
T1: SELECT * FROM users WHERE salary > 5500 → Bob, Charlie (новые СТРОКИ в диапазоне)
Проблема: появились новые строки, соответствующие условию
Уровни изоляции транзакций
Разные уровни защищают от разных аномалий:
| Уровень | Dirty Read | Non-repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | ✓ | ✓ | ✓ |
| READ COMMITTED | ✗ | ✓ | ✓ |
| REPEATABLE READ | ✗ | ✗ | ✓ |
| SERIALIZABLE | ✗ | ✗ | ✗ |
PostgreSQL и MySQL используют разные стратегии!
Защита от Phantom Read
1. SERIALIZABLE уровень
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
SELECT * FROM users WHERE salary > 5500;
-- База блокирует все изменения в этом диапазоне
-- Другие транзакции не могут INSERT/UPDATE/DELETE в диапазон
COMMIT;
2. Range Lock (в некоторых БД)
-- PostgreSQL: REPEATABLE READ уже защищает от фантомов
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM users WHERE salary > 5500 FOR UPDATE;
-- Блокирует диапазон
COMMIT;
3. Явная блокировка
// Java с использованием SELECT FOR UPDATE
String sql = "SELECT * FROM users WHERE salary > ? FOR UPDATE";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setInt(1, 5500);
ResultSet rs = stmt.executeQuery();
// Даже если другие транзакции попытаются INSERT, будут ждать
REPEATABLE READ в PostgreSQL vs MySQL
PostgreSQL 9.1+:
- REPEATABLE READ защищает от фантомных чтений
- Использует MVCC (Multi-Version Concurrency Control)
- Предотвращает видение новых строк
-- PostgreSQL (безопасно)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM users WHERE salary > 5500; -- 1
-- (другая транзакция INSERT Charlie)
SELECT COUNT(*) FROM users WHERE salary > 5500; -- 1 (не 2!)
COMMIT;
MySQL (при InnoDB):
- REPEATABLE READ НЕ гарантирует защиту от фантомов
- Нужен SERIALIZABLE или SELECT FOR UPDATE
-- MySQL (уязвимо при REPEATABLE READ)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM users WHERE salary > 5500; -- 1
-- (другая транзакция INSERT Charlie)
SELECT COUNT(*) FROM users WHERE salary > 5500; -- 2 (фантомное чтение!)
COMMIT;
Реальный пример: Инвентарь
public void processLowStockItems() throws Exception {
try (Connection conn = getConnection()) {
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
conn.setAutoCommit(false);
// Запрос 1: найти товары с низким запасом
String query = "SELECT * FROM products WHERE quantity < 10";
List<Product> products = new ArrayList<>();
try (PreparedStatement stmt = conn.prepareStatement(query)) {
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
products.add(new Product(rs));
}
}
System.out.println("Найдено товаров: " + products.size());
// Логика обработки...
Thread.sleep(1000);
// Запрос 2: снова проверяем низкий запас
// При SERIALIZABLE количество товаров не изменится
// При REPEATABLE READ могут появиться новые товары (фантомы)
conn.commit();
}
}
Как избежать фантомных чтений
-
Используй SERIALIZABLE (самый безопасный, но медленный)
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); -
Используй SELECT FOR UPDATE (заблокируй диапазон)
SELECT * FROM users WHERE salary > 5500 FOR UPDATE; -
Переделай логику (не полагайся на консистентность между запросами)
// Сделай все в одном запросе String sql = "SELECT COUNT(*) as count FROM users WHERE salary > ?"; // Вместо двух отдельных SELECT'ов -
Используй версионирование (проверяй изменения)
long version1 = getTableVersion(); List<Item> items = getItems(); long version2 = getTableVersion(); if (version1 != version2) { throw new ConcurrentModificationException(); }
Заключение
Фантомное чтение:
- Одна транзакция видит НОВЫЕ СТРОКИ, добавленные другой
- Результаты одного и того же SELECT отличаются
- Защита: SERIALIZABLE, SELECT FOR UPDATE или изменение логики
- PostgreSQL (REPEATABLE READ) защищает, MySQL нет
- Важно знать особенности конкретной БД
- Выбирай правильный уровень изоляции для твоего случая