Что будет, если прочитать незакоммиченные данные
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Прочтение незакоммиченных данных вызывает Dirty Read - нарушение изоляции
Что такое Dirty Read
Dirty Read - это ситуация, когда одна транзакция читает данные, которые были изменены другой транзакцией, но ещё не закоммичены. Если вторая транзакция откатится (rollback), то первая прочитала "грязные" (dirty) данные, которые никогда не существовали в БД.
Это серьёзное нарушение ACID принципов, особенно Isolation (изоляция).
Пример Dirty Read
// Транзакция 1: Начисление бонуса
@Transactional
public void addBonus(String userId, double amount) {
User user = userRepository.findById(userId).orElseThrow();
user.setBalance(user.getBalance() + amount); // Изменение в памяти
userRepository.save(user);
// Если сейчас прочитать в другой транзакции - получим новое значение
if (someErrorOccurs()) {
throw new RuntimeException("Ошибка при начислении");
// Транзакция откатится - этого бонуса не будет в БД
}
}
// Параллельно - Транзакция 2: Проверка баланса (READ_UNCOMMITTED)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public double checkBalance(String userId) {
User user = userRepository.findById(userId).orElseThrow();
return user.getBalance(); // DIRTY READ! Если TX1 откатится, это число неправильно
}
Уровни изоляции транзакций в SQL
READ_UNCOMMITTED - самый низкий уровень, разрешает Dirty Read:
@Service
public class DangerousTransactionService {
@Autowired
private UserRepository userRepository;
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public double getRiskBalance(String userId) {
// Может прочитать незакоммиченные данные
User user = userRepository.findById(userId).orElseThrow();
return user.getBalance();
}
}
READ_COMMITTED - предотвращает Dirty Read (по умолчанию в большинстве БД):
@Service
public class SafeTransactionService {
@Autowired
private UserRepository userRepository;
@Transactional(isolation = Isolation.READ_COMMITTED) // Стандарт
public double getSafeBalance(String userId) {
// Читает только закоммиченные данные
User user = userRepository.findById(userId).orElseThrow();
return user.getBalance();
}
}
Практический сценарий с последствиями
// БД: User(id=1, balance=100)
// Транзакция 1 (начинается первой)
@Transactional
public void transferMoney(String userId, double amount) {
User user = userRepository.findById(userId); // balance = 100
user.setBalance(user.getBalance() - amount); // balance = 100 - 50 = 50
userRepository.save(user); // Изменено в памяти, но ещё не закоммичено
// В этот момент: БД показывает 100, но в TX1 - 50
sendBankTransfer(amount); // Отправить деньги в банк
if (!bankConfirmed()) { // Банк отказал
throw new TransactionException("Transfer failed");
// ROLLBACK! balance остаётся 100 в БД
}
}
// Параллельно - Транзакция 2 (с READ_UNCOMMITTED - ОПАСНО!)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public double checkBalance(String userId) {
User user = userRepository.findById(userId);
double balance = user.getBalance(); // Прочитала 50!
// Но если TX1 откатится, в БД останется 100
// Мы показали пользователю неправильный баланс
return balance; // Возвращаем 50 - DIRTY READ!
}
Сравнение уровней изоляции
@Service
public class IsolationLevelComparison {
// LEVEL 1: READ_UNCOMMITTED - ОПАСНО, разрешает все аномалии
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void level1_AllProblems() {
// Может произойти: Dirty Read, Non-Repeatable Read, Phantom Read
}
// LEVEL 2: READ_COMMITTED - БЕЗ Dirty Read (по умолчанию)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void level2_Safe() {
// Предотвращает: Dirty Read
// Допускает: Non-Repeatable Read, Phantom Read
}
// LEVEL 3: REPEATABLE_READ - БЕЗ Dirty и Non-Repeatable Read
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void level3_Safer() {
// Предотвращает: Dirty Read, Non-Repeatable Read
// Допускает: Phantom Read
}
// LEVEL 4: SERIALIZABLE - Максимальная безопасность, но медленно
@Transactional(isolation = Isolation.SERIALIZABLE)
public void level4_MostSafe() {
// Предотвращает все аномалии
// Но может быть очень медленно - блокирует конкурирующие транзакции
}
}
Как предотвратить Dirty Read
Вариант 1: Использовать правильный уровень изоляции
@Configuration
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
tm.setDefaultIsolationLevel(Connection.TRANSACTION_READ_COMMITTED);
return tm;
}
}
@Service
public class BankingService {
// Используем READ_COMMITTED по умолчанию
@Transactional
public double getBalance(String accountId) {
Account account = accountRepository.findById(accountId).orElseThrow();
return account.getBalance(); // Безопасно - только закоммиченные данные
}
}
Вариант 2: Явное повышение уровня для критичных операций
@Service
public class CriticalFinanceService {
@Autowired
private AccountRepository accountRepository;
// Для критичных операций используем REPEATABLE_READ или SERIALIZABLE
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void creditAccountAtomically(String accountId, double amount) {
Account account = accountRepository.findById(accountId)
.orElseThrow(() -> new AccountNotFoundException());
// Все последующие читания будут видеть одни и те же данные
// даже если другие транзакции их изменяют
account.setBalance(account.getBalance() + amount);
accountRepository.save(account);
// COMMIT
}
}
Вариант 3: Блокировки (Pessimistic Locking)
@Service
public class LockedTransactionService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transferWithLock(String fromId, String toId, double amount) {
// Заблокируем счета на запись
Account fromAccount = accountRepository.findByIdWithLock(fromId);
Account toAccount = accountRepository.findByIdWithLock(toId);
// Пока эта транзакция активна, никто другой не сможет менять эти строки
fromAccount.setBalance(fromAccount.getBalance() - amount);
toAccount.setBalance(toAccount.getBalance() + amount);
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
// Repository с pessimistic lock
@Repository
public interface AccountRepository extends JpaRepository<Account, String> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdWithLock(@Param("id") String id);
}
Вариант 4: Optimistic Locking (версионирование)
@Entity
@Table(name = "accounts")
public class Account {
@Id
private String id;
private double balance;
@Version // Версия инкрементируется при каждом обновлении
private Long version;
}
@Service
public class OptimisticLockService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void updateBalance(String accountId, double newBalance) {
Account account = accountRepository.findById(accountId).orElseThrow();
account.setBalance(newBalance);
try {
accountRepository.save(account);
} catch (OptimisticLockingFailureException e) {
// Другая транзакция изменила этот счёт
// Нужно повторить или уведомить пользователя
log.error("Concurrent modification detected", e);
throw new ConcurrentModificationException();
}
}
}
Практический совет
// ❌ НИКОГДА не делай так
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void dangerousOperation() {
// Это открывает двери для dirty reads
}
// ✅ Используй правильный уровень
@Transactional(isolation = Isolation.READ_COMMITTED) // По умолчанию
public void safeOperation() {
// Защищено от dirty reads
}
// ✅ Для критичных финансовых операций
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void criticalFinancialOperation() {
// Максимальная безопасность для бизнес-логики
}
Вывод
Забывание о Dirty Read - частая ошибка в финансовых системах. Всегда помни:
- READ_UNCOMMITTED опасен - используй только если очень знаешь что делаешь
- READ_COMMITTED по умолчанию - хороший баланс между безопасностью и производительностью
- REPEATABLE_READ для критичных операций - финансы, инвентарь, бронирование
- Выбирай вариант с учётом требований - не переусложняй если не нужно