← Назад к вопросам
В чем заключается аномалия фантомное чтение
3.0 Senior🔥 61 комментариев
#Базы данных и SQL
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Аномалия "Фантомное чтение" (Phantom Read)
Краткий ответ
"Фантомное чтение" (Phantom Read) — это аномалия изолированности транзакций, когда одна транзакция читает набор строк, удовлетворяющих условию WHERE, а другая транзакция в это время добавляет новые строки, которые тоже соответствуют этому условию. При повторном чтении первая транзакция видит "фантомные" новые строки.
Определение и различие от других аномалий
Три аномалии изолированности
| Аномалия | Описание | Когда происходит |
|---|---|---|
| Dirty Read | Одна транзакция читает несохранённые данные другой | Isolation < READ_COMMITTED |
| Non-repeatable Read | При повторном чтении одной СТРОКИ видны разные значения | Isolation < REPEATABLE_READ |
| Phantom Read | При повторном чтении WHERE условия видны новые СТРОКИ | Isolation < SERIALIZABLE |
Ключевое отличие
Non-repeatable Read: Одна СТРОКА изменилась (UPDATE)
Phantom Read: Количество СТРОК изменилось (INSERT/DELETE)
Пример Phantom Read
Сценарий
Табль accounts:
┌──────┬──────────┬─────────┐
│ id │ name │ balance │
├──────┼──────────┼─────────┤
│ 1 │ Alice │ 1000 │
│ 2 │ Bob │ 500 │
└──────┴──────────┴─────────┘
Временная шкала
Время │ Транзакция 1 │ Транзакция 2
──────┼───────────────────────────┼─────────────────────────────
│ BEGIN TRANSACTION │
T1 │ SELECT COUNT(*) WHERE │ BEGIN TRANSACTION
│ balance > 700 │
│ Результат: 1 (только Alice)│
T2 │ │ INSERT INTO accounts
│ │ (Charlie, 800)
T3 │ │ COMMIT
T4 │ SELECT COUNT(*) WHERE │
│ balance > 700 │
│ Результат: 2 (Alice + Charlie!) │ ← Это Phantom Read!
T5 │ COMMIT │
Код примера на Java + Hibernate
Пример 1: Простая демонстрация
@Service
@Transactional(isolation = Isolation.READ_COMMITTED)
public class PhantomReadExample {
@Autowired
private AccountRepository accountRepository;
public void demonstratePhantomRead() throws InterruptedException {
// Первый запрос
List<Account> firstRead = accountRepository.findAllWithBalance(700);
System.out.println("Первое чтение: " + firstRead.size()); // 1
// В это время другой поток добавляет новый аккаунт
Thread.sleep(1000); // Даём время другой транзакции
// Второй запрос
List<Account> secondRead = accountRepository.findAllWithBalance(700);
System.out.println("Второе чтение: " + secondRead.size()); // 2 (фантомное!)
}
}
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
@Query("SELECT a FROM Account a WHERE a.balance > :minBalance")
List<Account> findAllWithBalance(@Param("minBalance") BigDecimal minBalance);
}
Пример 2: Phantom Read в многопоточной среде
@Service
public class TransactionService {
@Autowired
private AccountRepository accountRepository;
@Autowired
private ApplicationContext context;
public void testPhantomRead() throws InterruptedException {
// Стартуем две транзакции в параллели
Thread t1 = new Thread(() -> transactionT1());
Thread t2 = new Thread(() -> transactionT2());
t1.start();
t2.start();
t1.join();
t2.join();
}
@Transactional(isolation = Isolation.READ_COMMITTED)
private void transactionT1() {
System.out.println("[T1] Начало транзакции");
List<Account> accounts = accountRepository.findAllWithBalance(700);
System.out.println("[T1] Первое чтение: " + accounts.size() + " аккаунтов");
try {
Thread.sleep(2000); // Ждём, пока T2 добавит запись
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
List<Account> accountsAgain = accountRepository.findAllWithBalance(700);
System.out.println("[T1] Второе чтение: " + accountsAgain.size() + " аккаунтов");
if (accounts.size() != accountsAgain.size()) {
System.out.println("⚠️ PHANTOM READ ОБНАРУЖЕНО!");
}
}
@Transactional(isolation = Isolation.READ_COMMITTED)
private void transactionT2() {
try {
Thread.sleep(500); // Даём T1 время на первый запрос
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("[T2] Вставляю новый аккаунт");
Account newAccount = new Account("Charlie", new BigDecimal("800"));
accountRepository.save(newAccount);
System.out.println("[T2] Новый аккаунт добавлен");
}
}
Уровни изолированности и защита от Phantom Read
SQL уровни изолированности
Уровень │ Dirty Read │ Non-Repeatable │ Phantom Read
─────────────────────┼────────────┼────────────────┼──────────────
READ_UNCOMMITTED │ ✓ (есть) │ ✓ │ ✓
READ_COMMITTED │ ✗ │ ✓ (есть) │ ✓
REPEATABLE_READ │ ✗ │ ✗ │ ✓ (есть)
SERIALIZABLE │ ✗ │ ✗ │ ✗
В Java/Spring
// Уязвим к Phantom Read
@Transactional(isolation = Isolation.READ_COMMITTED)
public void unsafeMethod() { }
// Защищён от Phantom Read
@Transactional(isolation = Isolation.SERIALIZABLE)
public void safeMethod() { }
Решение 1: SERIALIZABLE изолированность
@Service
public class AccountTransferService {
@Autowired
private AccountRepository accountRepository;
@Transactional(isolation = Isolation.SERIALIZABLE)
public BigDecimal calculateAverageBalance() {
List<Account> accounts = accountRepository.findAllWithBalance(700);
// Теперь никто не может добавить/удалить аккаунты
// пока выполняется эта транзакция
return calculateAverage(accounts);
}
}
Недостаток: SERIALIZABLE может быть медленным из-за блокировок.
Решение 2: Явная блокировка строк
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
// Используем FOR UPDATE для блокировки
@Query(value =
"SELECT a FROM Account a WHERE a.balance > :minBalance FOR UPDATE",
nativeQuery = false)
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Account> findAllWithBalanceForUpdate(@Param("minBalance") BigDecimal minBalance);
}
@Service
public class LockedAccountService {
@Transactional
public BigDecimal getAccountCount() {
// Эта строка будет заблокирована для других транзакций
List<Account> accounts = accountRepository.findAllWithBalanceForUpdate(new BigDecimal("700"));
return new BigDecimal(accounts.size());
}
}
Решение 3: Оптимистическая блокировка
@Entity
public class Account {
@Id
private Long id;
private String name;
private BigDecimal balance;
@Version // Версия для оптимистической блокировки
private Long version;
}
@Service
public class OptimisticLockService {
@Transactional
public void transferMoney() {
Account account = accountRepository.findById(1L).orElseThrow();
account.setBalance(account.getBalance().subtract(new BigDecimal("100")));
accountRepository.save(account);
// Если версия изменилась — выбросится OptimisticLockException
}
}
Решение 4: Event Sourcing / CQRS
Для систем с высокой нагрузкой:
@Service
public class EventSourcingService {
@Autowired
private EventStore eventStore;
@Transactional
public void processMoneyTransfer(MoneyTransferCommand cmd) {
// Записываем событие (основной источник правды)
AccountCreatedEvent event = new AccountCreatedEvent(
cmd.getAccountId(),
cmd.getAmount()
);
eventStore.append(event);
// Фантомные строки невозможны, так как:
// 1. События идемпотентны
// 2. Порядок гарантирован
// 3. Нет конкурирующего доступа
}
}
Практический совет
Когда использовать SERIALIZABLE?
// ✓ Критичные финансовые операции
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transferMoney(Long from, Long to, BigDecimal amount) { }
// ✓ Отчёты с точными цифрами
@Transactional(isolation = Isolation.SERIALIZABLE)
public ReportData generateReport(Date startDate) { }
Когда хватит READ_COMMITTED?
// ✓ Чтение кеш-данных
@Transactional(isolation = Isolation.READ_COMMITTED)
public User getUserById(Long id) { }
// ✓ Обновление счётчиков (с небольшой погрешностью)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void incrementViewCount(Long postId) { }
Итоги
- Phantom Read — это появление новых строк при повторном SELECT с WHERE
- Отличается от Non-Repeatable Read — там изменяется значение одной строки
- Защита:
- SERIALIZABLE изолированность (медленно)
- Явная блокировка WITH (SELECT FOR UPDATE)
- Оптимистическая блокировка @Version
- Event Sourcing для высоконагруженных систем
- На практике: используй READ_COMMITTED по умолчанию, SERIALIZABLE только для критичных операций