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

Какой уровень изоляции поможет избежать одновременную перезапись результата другого изменения?

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

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

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

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

# Уровень изоляции для избежания Lost Update

Проблема, которую вы описываете, называется Lost Update (потеря обновления). Это происходит, когда две транзакции читают одно и то же значение, обновляют его независимо и записывают обновленные значения, потеряв изменения первой транзакции.

Пример Lost Update

Исходное значение balance = 100

Транзакция 1:                    Транзакция 2:
1. read balance = 100            1. read balance = 100
2. balance = 100 + 50 = 150      2. balance = 100 - 20 = 80
3. write balance = 150           3. write balance = 80

Результат: balance = 80 (потеря +50)

Уровни изоляции в SQL

1. READ UNCOMMITTED

Низший уровень. Может читать незафиксированные ("грязные") данные.

Дозволяет Dirty Read, Non-repeatable Read, Phantom Read, Lost Update
❌ Не защищает от Lost Update

2. READ COMMITTED

Дефолтный в PostgreSQL. Читает только зафиксированные данные.

Дозволяет Non-repeatable Read, Phantom Read, Lost Update
❌ Не защищает от Lost Update

3. REPEATABLE READ

Гарантирует, что одна транзакция видит одинаковые данные при повторном чтении.

Дозволяет Phantom Read, Lost Update (в некоторых БД)
⚠ Зависит от реализации БД

4. SERIALIZABLE

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

Не дозволяет ничего (ни Dirty Read, ни Non-repeatable Read, ни Phantom Read)
✅ ЗАЩИЩАЕТ от Lost Update

Таблица проблем и уровней

ПроблемаREAD UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
Dirty Read
Non-repeatable Read
Phantom Read
Lost Update

Решение: SERIALIZABLE уровень

Java с Hibernate

@Service
public class AccountService {
    private final accountRepository;
    
    // SERIALIZABLE уровень
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();
        
        from.withdraw(amount);
        to.deposit(amount);
        
        accountRepository.save(from);
        accountRepository.save(to);
    }
}

Java с SQL

public void transfer(Connection conn, Long fromId, Long toId, BigDecimal amount) throws SQLException {
    // Установить SERIALIZABLE уровень
    conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
    
    try {
        conn.setAutoCommit(false);
        
        String sql = "SELECT balance FROM accounts WHERE id = ? FOR UPDATE";
        PreparedStatement stmt = conn.prepareStatement(sql);
        
        stmt.setLong(1, fromId);
        ResultSet rs = stmt.executeQuery();
        rs.next();
        BigDecimal balance = rs.getBigDecimal("balance");
        
        if (balance.compareTo(amount) >= 0) {
            // Обновить счёты
            conn.commit();
        } else {
            conn.rollback();
        }
    } finally {
        conn.setAutoCommit(true);
    }
}

PostgreSQL

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

  -- Попытка обновления
  UPDATE accounts SET balance = balance + 50 WHERE id = 1;
  
  -- Если другая транзакция уже обновила этот ряд,
  -- PostgreSQL вернёт ошибку serialization_failure
  
COMMIT;  -- Или ROLLBACK если конфликт

Oшибка PostgreSQL при конфликте:

ERROR: could not serialize access due to concurrent update

MySQL

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

START TRANSACTION;
  SELECT * FROM accounts WHERE id = 1;
  UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;

Альтернативы SERIALIZABLE

Вместо SERIALIZABLE (которая замедляет систему), можно использовать:

1. Pessimistic Locking (FOR UPDATE)

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    @Query("SELECT a FROM Account a WHERE a.id = :id")
    @Lock(LockModeType.PESSIMISTIC_WRITE)  // FOR UPDATE
    Optional<Account> findByIdForUpdate(@Param("id") Long id);
}

@Service
public class AccountService {
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        // Захватить блокировку на запись
        Account from = accountRepository.findByIdForUpdate(fromId).orElseThrow();
        Account to = accountRepository.findByIdForUpdate(toId).orElseThrow();
        
        from.withdraw(amount);
        to.deposit(amount);
        
        accountRepository.save(from);
        accountRepository.save(to);
    }
}

2. Optimistic Locking (Version Field)

@Entity
public class Account {
    @Id
    private Long id;
    
    private BigDecimal balance;
    
    @Version  // Автоматическое управление версией
    private Long version;
}

@Service
public class AccountService {
    @Transactional  // READ_COMMITTED достаточно
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();
        
        from.withdraw(amount);
        to.deposit(amount);
        
        try {
            accountRepository.save(from);
            accountRepository.save(to);
        } catch (OptimisticLockException e) {
            // Повторить попытку
            transfer(fromId, toId, amount);
        }
    }
}

3. SELECT FOR UPDATE (явная блокировка)

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    @Query(value = "SELECT * FROM accounts WHERE id = :id FOR UPDATE", nativeQuery = true)
    Optional<Account> findByIdWithLock(@Param("id") Long id);
}

Лучшие практики

  1. Избегай SERIALIZABLE в продакшене — снижает пропускную способность на 50-90%
  2. Используй Pessimistic Locking для критичных операций (финансы)
  3. Используй Optimistic Locking когда конфликты редки
  4. REPEATABLE READ достаточно для большинства приложений
  5. Тестируй Lost Update в integration тестах
  6. Документируй уровень изоляции для каждой критичной операции

Вывод

Ответ: SERIALIZABLE уровень полностью защищает от Lost Update.

Но в реальных проектах используют:

  • Pessimistic Locking (FOR UPDATE) для критичных операций
  • Optimistic Locking (Version Field) для нечастых конфликтов
  • Appropriate Isolation Level + бизнес-логика для баланса между безопасностью и производительностью