← Назад к вопросам

Что такое фантомное чтение?

2.7 Senior🔥 151 комментариев
#Базы данных и SQL#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое фантомное чтение?

Фантомное чтение (Phantom Read) — это проблема в БД, когда одна транзакция видит строки, которые были добавлены другой транзакцией, нарушая консистентность данных.

Определение

Фантомное чтение происходит когда:

  1. Транзакция A выполняет SELECT с WHERE условием
  2. Транзакция B вставляет (INSERT) или удаляет (DELETE) строки, которые соответствуют условию A
  3. Транзакция A выполняет тот же SELECT еще раз
  4. Результаты отличаются! Появились новые строки (фантомы)

Пример

Исходное состояние таблицы 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 ReadNon-repeatable ReadPhantom 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();
    }
}

Как избежать фантомных чтений

  1. Используй SERIALIZABLE (самый безопасный, но медленный)

    connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
    
  2. Используй SELECT FOR UPDATE (заблокируй диапазон)

    SELECT * FROM users WHERE salary > 5500 FOR UPDATE;
    
  3. Переделай логику (не полагайся на консистентность между запросами)

    // Сделай все в одном запросе
    String sql = "SELECT COUNT(*) as count FROM users WHERE salary > ?";
    // Вместо двух отдельных SELECT'ов
    
  4. Используй версионирование (проверяй изменения)

    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 нет
  • Важно знать особенности конкретной БД
  • Выбирай правильный уровень изоляции для твоего случая