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

Приведи пример когда возникает Phantom Read

2.3 Middle🔥 201 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью#ORM и Hibernate

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

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

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

Phantom Read: Проблема с уровнями изоляции

Phantom Read — это проблема с конкурентностью транзакций, когда одна транзакция видит новые строки, добавленные другой транзакцией.

Что такое Phantom Read

Phantom = призрак. Новые данные появляются "из ниоткуда".

Пример 1: Счет заказов

Табличка Orders:
┌─────┬──────────┬────────┐
│ id  │ customer │ amount │
├─────┼──────────┼────────┤
│ 1   │ user_1   │ 100    │
│ 2   │ user_1   │ 200    │
└─────┴──────────┴────────┘

Транзакция 1 (T1): Подсчёт заказов user_1

BEGIN;

-- Время 1: Подсчитали
SELECT COUNT(*) FROM orders WHERE customer = 'user_1';
Результат: 2 заказа

-- Делаем какие-то вычисления на основе этого числа...
-- (например, расчет скидки за количество заказов)

Транзакция 2 (T2): Добавление нового заказа (запускается параллельно)

BEGIN;
INSERT INTO orders VALUES (3, 'user_1', 150);
COMMIT;

Продолжение Транзакции 1

-- Время 2: Опять подсчитали
SELECT COUNT(*) FROM orders WHERE customer = 'user_1';
Результат: 3 заказа (!!!) Phantom!

-- Ещё одна проверка
SELECT * FROM orders WHERE customer = 'user_1';
Получили строку которая "не было" раньше

COMMIT;

Проблема: Transакция 1 дважды выполнила одинаковый запрос и получила разные результаты.

Пример 2: Отчет по продажам

Табличка Sales:
┌────┬──────┬────────┐
│ id │ date │ amount │
├────┼──────┼────────┤
│ 1  │ 2024 │ 1000   │
│ 2  │ 2024 │ 2000   │
└────┴──────┴────────┘

T1: Составление отчета (SERIALIZABLE? Нет, READ_COMMITTED)

BEGIN (READ_COMMITTED);

-- Подсчитали сумму на дату 2024
SELECT SUM(amount) FROM sales WHERE date = '2024';
Результат: 3000

T2: Параллельная вставка (другая сессия)

INSERT INTO sales VALUES (3, '2024', 5000);
COMMIT;

Продолжение T1:

-- Проверяем ещё раз
SELECT SUM(amount) FROM sales WHERE date = '2024';
Результат: 8000 (!!!) Phantom!

COMMIT;

Уровни изоляции и Phantom Read

┌──────────────────┬────────┬────────────┬─────────────────┐
│ Уровень          │ Dirty  │ Non-Repeat │ Phantom Read    │
│                  │ Read   │ Read       │                 │
├──────────────────┼────────┼────────────┼─────────────────┤
│ READ_UNCOMMITTED │ ✓ Есть │ ✓ Есть     │ ✓ Есть          │
│ READ_COMMITTED   │ ✗ Нет  │ ✓ Есть     │ ✓ Есть          │
│ REPEATABLE_READ  │ ✗ Нет  │ ✗ Нет      │ ✓ Есть (MySQL)  │
│ SERIALIZABLE     │ ✗ Нет  │ ✗ Нет      │ ✗ Нет           │
└──────────────────┴────────┴────────────┴─────────────────┘

Как избежать Phantom Read

Способ 1: SERIALIZABLE уровень

@Transactional(isolation = Isolation.SERIALIZABLE)
public void countOrdersForUser(String userId) {
    List<Order> orders1 = orderRepository.findByUser(userId);
    System.out.println("First count: " + orders1.size());
    
    // Другая транзакция может вставить новый заказ
    try {
        Thread.sleep(5000); // Имитация работы
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    List<Order> orders2 = orderRepository.findByUser(userId);
    System.out.println("Second count: " + orders2.size());
    
    // С SERIALIZABLE оба результата будут одинаковыми
    // Минус: очень медленно
}

Способ 2: SELECT FOR UPDATE (Range Lock)

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("SELECT o FROM Order o WHERE o.customer = :customer FOR UPDATE")
    List<Order> findByCustomerForUpdate(@Param("customer") String customer);
}

@Service
public class OrderService {
    @Transactional
    public void processOrders(String customerId) {
        // Заблокировали все строки для этого customer
        List<Order> orders = orderRepository.findByCustomerForUpdate(customerId);
        
        // Другие транзакции не могут добавить новый заказ
        // (будут ждать пока мы не освободим lock)
        
        int totalAmount = orders.stream()
            .mapToInt(Order::getAmount)
            .sum();
        
        System.out.println("Total: " + totalAmount);
    }
}

Способ 3: Application-Level Lock

@Service
public class OrderService {
    private final Lock orderProcessingLock = new ReentrantLock();
    
    public void processOrders(String customerId) {
        orderProcessingLock.lock();
        try {
            // Критическая секция
            List<Order> orders = orderRepository.findByCustomer(customerId);
            // ...
        } finally {
            orderProcessingLock.unlock();
        }
    }
}

Способ 4: Distributed Lock (для микросервисов)

@Service
public class OrderService {
    @Autowired
    private RedisLockProvider lockProvider;
    
    public void processOrders(String customerId) {
        String lockName = "order_" + customerId;
        
        try (SimpleLock lock = lockProvider.lock(lockName, Duration.ofSeconds(30))) {
            // Только один сервис может выполнять это одновременно
            List<Order> orders = orderRepository.findByCustomer(customerId);
            // ...
        }
    }
}

Пример в коде Spring Data

@Entity
public class Order {
    @Id
    private Long id;
    private String customer;
    private int amount;
}

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query(value = "SELECT * FROM orders WHERE customer = :customer FOR UPDATE",
           nativeQuery = true)
    List<Order> findByCustomerWithLock(@Param("customer") String customer);
}

@Service
public class OrderProcessingService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    // ✓ Безопасно от Phantom Read
    @Transactional
    public int calculateTotalAmount(String customerId) {
        List<Order> orders = orderRepository.findByCustomerWithLock(customerId);
        return orders.stream()
            .mapToInt(Order::getAmount)
            .sum();
    }
    
    // ✗ Опасно для Phantom Read (два запроса)
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void unsafeCountAndSum(String customerId) {
        int count = (int) orderRepository.countByCustomer(customerId);
        int sum = orderRepository.sumAmountByCustomer(customerId);
        
        // Между первым и вторым запросом может быть добавлена новая строка
        // count и sum будут несогласованными
    }
}

На собеседовании

Хороший ответ:

"Phantom Read — это когда одна транзакция выполняет одинаковый SELECT два раза и получает разные результаты, потому что другая транзакция добавила новые строки.

Пример: подсчитываю количество заказов user_1, получил 2. Другая транзакция добавляет заказ. Подсчитываю ещё раз — получил 3.

Чтобы избежать:

  1. Использовать SERIALIZABLE (медленно)
  2. SELECT FOR UPDATE — блокирует строки
  3. Кешировать результаты если не критично
  4. Использовать optimistic locking с версионированием

В PostgreSQL REPEATABLE_READ блокирует Phantom Read благодаря MVCC, но MySQL нет."

Ключевые выводы

  • Phantom Read = новые строки появляются между запросами одной транзакции
  • READ_COMMITTED и REPEATABLE_READ уязвимы
  • SERIALIZABLE защищает, но медленно
  • SELECT FOR UPDATE — практичное решение
  • В PostgreSQL лучше, чем MySQL по умолчанию
  • Для микросервисов нужны distributed locks