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

Какие знаешь проблемы при неправильном выборе уровня изоляции транзакции?

1.7 Middle🔥 61 комментариев
#Базы данных и SQL

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

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

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

Уровни изоляции транзакций: проблемы и последствия

Уровень изоляции транзакции определяет, как одновременные транзакции взаимодействуют друг с другом. Неправильный выбор уровня изоляции может привести к серьёзным проблемам с консистентностью данных, производительностью и надежностью системы.

Четыре стандартных уровня изоляции (по ACID)

1. READ UNCOMMITTED (Грязное чтение)

Самый низкий уровень изоляции. Транзакция может читать незафиксированные изменения других транзакций.

// Транзакция A
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readDirtyData() {
    // Может прочитать данные, которые ещё не закоммичены
    User user = userRepository.findById(1L).orElse(null);
}

Проблема — Dirty Read:

Транзакция 1: UPDATE account SET balance = 100 WHERE id = 1
Транзакция 2: READ balance = 100 (но Транзакция 1 ещё не commit)
Транзакция 1: ROLLBACK
Транзакция 2: использует balance = 100, но на самом деле это было отменено

2. READ COMMITTED (Чтение подтвёрженных данных)

По умолчанию во многих БД. Транзакция может читать только закоммиченные изменения.

@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedData() {
    // Может произойти Non-Repeatable Read
    User user = userRepository.findById(1L).orElse(null);
    // Другая транзакция может изменить этого пользователя
}

Проблема — Non-Repeatable Read (Неповторяемое чтение):

Транзакция 1: SELECT balance = 100
Транзакция 2: UPDATE balance = 200, COMMIT
Транзакция 1: SELECT balance = 200 (данные изменились внутри одной транзакции)

3. REPEATABLE READ (Повторяемое чтение)

Транзакция видит консистентный snapshot данных. Но возможны phantom reads — появление новых строк.

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableRead() {
    // Одни и те же SELECT вернут одни и те же данные
    List<User> users = userRepository.findAll();
    // Но может появиться новая строка, вставленная другой транзакцией
}

Проблема — Phantom Read (Фантомное чтение):

Транзакция 1: SELECT COUNT(*) FROM orders WHERE status = pending = 5
Транзакция 2: INSERT new order WITH status = pending, COMMIT
Транзакция 1: SELECT COUNT(*) FROM orders WHERE status = pending = 6 (Phantom!)

4. SERIALIZABLE (Сериализуемость)

Высочайший уровень изоляции. Как будто транзакции выполняются последовательно, без параллелизма.

@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializable() {
    // Самый безопасный, но медленный уровень
    List<User> users = userRepository.findAll();
}

Матрица проблем по уровням изоляции

УровеньDirty ReadNon-RepeatablePhantom ReadПроизводительность
READ UNCOMMITTEDДаДаДаМаксимум
READ COMMITTEDНетДаДаХорошая
REPEATABLE READНетНетДаСредняя
SERIALIZABLEНетНетНетМинимум

Реальные проблемы и примеры

Проблема 1: Потеря изменений (Lost Update)

// Оба используют READ COMMITTED (по умолчанию в PostgreSQL)
Thread 1: @Transactional
public void incrementBalance1() {
    User user = userRepository.findById(1L).get();
    // balance = 100
    user.setBalance(user.getBalance() + 50); // 150
    userRepository.save(user);
}

Thread 2: @Transactional
public void incrementBalance2() {
    User user = userRepository.findById(1L).get();
    // balance = 100 (read committed, не видит изменения Thread 1)
    user.setBalance(user.getBalance() + 30); // 130
    userRepository.save(user);
}

// Результат: balance = 130, но должно быть 180

Решение — использовать Pessimistic Locking:

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT u FROM User u WHERE u.id = ?1")
User findByIdForUpdate(Long id);

Проблема 2: Race condition в бизнес-логике

// Bank transfer с READ COMMITTED
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    User from = userRepository.findById(fromId).get();
    User to = userRepository.findById(toId).get();
    
    if (from.getBalance().compareTo(amount) >= 0) {
        // Между проверкой и обновлением другая транзакция
        // может снять деньги
        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));
    }
    // Может случиться овердрафт!
}

Решение — SELECT FOR UPDATE (Pessimistic Lock):

@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    // Эта запись будет заблокирована для других транзакций
    User from = findByIdForUpdate(fromId);
    User to = findByIdForUpdate(toId);
    
    if (from.getBalance().compareTo(amount) >= 0) {
        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));
    }
}

Проблема 3: Snapshot skew при REPEATABLE READ

Применимо для расчётов:

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void calculateTotalBalance() {
    BigDecimal account1 = getBalance(1L); // 100
    BigDecimal account2 = getBalance(2L); // 100
    // Другая транзакция: transfer 50 с account1 на account2
    BigDecimal account1_again = getBalance(1L); // 50 (если пересчитаем)
    BigDecimal account2_again = getBalance(2L); // 150 (если пересчитаем)
    
    // Total = 50 + 150 = 200, но было 200 (деньги не потеряны)
    // Однако может произойти несоответствие в аналитике
}

Выбор правильного уровня изоляции

Используй READ COMMITTED для:

  • Большинства web-приложений
  • Высоконагруженных систем
  • Когда параллелизм важнее консистентности
@Transactional(isolation = Isolation.READ_COMMITTED)
public void webRequest() { ... }

Используй REPEATABLE READ для:

  • Финансовых операций
  • Операций с инвентарём
  • Когда нужна консистентность, но не максимальная
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void inventory() { ... }

Используй SERIALIZABLE для:

  • Критичных финансовых операций
  • Когда консистентность критична
  • Редких операций
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalTransfer() { ... }

Заключение

Выбор уровня изоляции — это компромисс между:

  • Консистентностью данных (безопасность)
  • Производительностью (пропускная способность)
  • Параллелизмом (одновременные пользователи)

Неправильный выбор приводит к потере данных, race conditions, или деградации производительности. Всегда тестируй с несколькими одновременными транзакциями!