Какой уровень изоляции поможет избежать одновременную перезапись результата другого изменения?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Уровень изоляции для избежания 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 UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE |
|---|---|---|---|---|
| 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);
}
Лучшие практики
- Избегай SERIALIZABLE в продакшене — снижает пропускную способность на 50-90%
- Используй Pessimistic Locking для критичных операций (финансы)
- Используй Optimistic Locking когда конфликты редки
- REPEATABLE READ достаточно для большинства приложений
- Тестируй Lost Update в integration тестах
- Документируй уровень изоляции для каждой критичной операции
Вывод
Ответ: SERIALIZABLE уровень полностью защищает от Lost Update.
Но в реальных проектах используют:
- Pessimistic Locking (FOR UPDATE) для критичных операций
- Optimistic Locking (Version Field) для нечастых конфликтов
- Appropriate Isolation Level + бизнес-логика для баланса между безопасностью и производительностью