Приведи пример когда возникает Phantom Read
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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.
Чтобы избежать:
- Использовать SERIALIZABLE (медленно)
- SELECT FOR UPDATE — блокирует строки
- Кешировать результаты если не критично
- Использовать optimistic locking с версионированием
В PostgreSQL REPEATABLE_READ блокирует Phantom Read благодаря MVCC, но MySQL нет."
Ключевые выводы
- Phantom Read = новые строки появляются между запросами одной транзакции
- READ_COMMITTED и REPEATABLE_READ уязвимы
- SERIALIZABLE защищает, но медленно
- SELECT FOR UPDATE — практичное решение
- В PostgreSQL лучше, чем MySQL по умолчанию
- Для микросервисов нужны distributed locks