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

Могут ли транзакции влиять друг на друга

2.0 Middle🔥 191 комментариев
#Другое

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

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

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

Могут ли транзакции влиять друг на друга

Да, транзакции могут влиять друг на друга. Это зависит от уровня изоляции (Isolation Level) и является одной из самых сложных тем в разработке.

ACID принципы

Atomicity — транзакция атомарна (либо всё, либо ничего) Consistency — консистентность данных Isolation — изоляция от других транзакций ← ВОТ ОТ ЧЕГО ОНА ЗАВИСИТ Durability — долговечность (после commit)

Уровни изоляции (в порядке возрастания)

1. READ UNCOMMITTED (самый опасный)

public class TransactionIsolation {
    
    // Транзакция 1: Читает неподтвержденные данные
    public void transaction1() {
        // balance = 1000
        balance -= 100;  // balance временно = 900
        // Не вызовем commit
    }
    
    // Транзакция 2: Читает "грязные" данные
    public void transaction2() {
        // Видит balance = 900 (хотя Транзакция 1 не завершена!)
        int currentBalance = getBalance();  // = 900
        
        // Если Транзакция 1 откатится, мы работаем с неверной цифрой
    }
}

Проблема: Dirty Read (грязное чтение)

Транзакция 1            Транзакция 2
UPDATE balance = 900
                        SELECT balance → 900
ROLLBACK
                        balance был 1000, но видит 900!

2. READ COMMITTED (по умолчанию в PostgreSQL)

public class ReadCommitted {
    
    // Транзакция 1
    public void transfer_from_account() {
        Account from = db.findById(1);        // balance = 1000
        from.balance -= 100;
        db.save(from);  // UPDATE, но еще не commit
        sleep(2000);    // Симулируем долгую операцию
        db.commit();    // Теперь committed
    }
    
    // Транзакция 2 (одновременно)
    public void get_balance() {
        Account from = db.findById(1);
        // Видит старое значение (1000), потому что UPDATE еще не committed
        System.out.println(from.balance);  // = 1000
        sleep(500);
        // Теперь Транзакция 1 закончилась
        Account from2 = db.findById(1);
        System.out.println(from2.balance);  // = 900 ← PHANTOM READ
    }
}

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

Транзакция 1            Транзакция 2
                        SELECT balance → 1000
UPDATE balance = 900
COMMIT
                        SELECT balance → 900  ← Разные значения!

3. REPEATABLE READ (по умолчанию в MySQL)

public class RepeatableRead {
    
    // Транзакция 1: Создает новую строку
    public void insertNewUser() {
        User user = new User("Bob", age = 25);
        db.save(user);
        db.commit();
    }
    
    // Транзакция 2: Читает коллекцию
    public void countUsers() {
        int count1 = db.query("SELECT COUNT(*) FROM users");
        // count1 = 10
        
        sleep(1000);
        
        // Транзакция 1 завершилась и добавила нового пользователя
        
        int count2 = db.query("SELECT COUNT(*) FROM users");
        // count2 = 11 ← PHANTOM ROW (новая строка)
    }
}

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

Транзакция 1            Транзакция 2
                        SELECT COUNT(*) → 10
INSERT user
COMMIT
                        SELECT COUNT(*) → 11  ← Появилась новая строка!

4. SERIALIZABLE (самый безопасный)

@Transactional(isolation = Isolation.SERIALIZABLE)
public void atomicTransfer(Long fromId, Long toId, BigDecimal amount) {
    // Вся транзакция выполняется как если бы она была одна
    // Никакие другие транзакции не могут вмешаться
    
    Account from = db.findById(fromId);     // Заблокирована строка
    Account to = db.findById(toId);         // Заблокирована строка
    
    from.balance -= amount;
    to.balance += amount;
    
    db.save(from);
    db.save(to);
    // Никто другой не может читать эти строки до commit
}

Преимущество: Транзакции не влияют друг на друга

Недостаток: Очень медленно (много блокировок, много deadlock'ов)

Таблица уровней изоляции

УровеньDirty ReadNon-Repeatable ReadPhantom ReadПроизводительность
READ UNCOMMITTEDОчень быстро
READ COMMITTEDБыстро
REPEATABLE READСредне
SERIALIZABLEОчень медленно

Практический пример: Банковский трансфер

@Service
public class BankService {
    
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void unsafeTransfer(Long from, Long to, BigDecimal amount) {
        Account accountFrom = accountRepository.findById(from);
        
        // ОПАСНО: другая транзакция может изменить accountFrom
        if (accountFrom.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException();
        }
        
        // Между проверкой и обновлением
        // другая транзакция может вывести деньги
        
        accountFrom.setBalance(
            accountFrom.getBalance().subtract(amount)
        );
        Account accountTo = accountRepository.findById(to);
        accountTo.setBalance(
            accountTo.getBalance().add(amount)
        );
        
        accountRepository.save(accountFrom);
        accountRepository.save(accountTo);
    }
    
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void safeTransfer(Long from, Long to, BigDecimal amount) {
        // БЕЗОПАСНО: обе строки заблокированы
        Account accountFrom = accountRepository.findById(from);
        Account accountTo = accountRepository.findById(to);
        
        if (accountFrom.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException();
        }
        
        accountFrom.setBalance(
            accountFrom.getBalance().subtract(amount)
        );
        accountTo.setBalance(
            accountTo.getBalance().add(amount)
        );
        
        accountRepository.save(accountFrom);
        accountRepository.save(accountTo);
    }
}

Как транзакции влияют друг на друга

1. Lost Update (потерянное обновление)

Транзакция 1            Транзакция 2
SELECT balance = 1000
                        SELECT balance = 1000
UPDATE balance = 900
COMMIT
                        UPDATE balance = 900
                        COMMIT

Результат: balance = 900 (первое обновление потеряно)

2. Deadlock

Транзакция 1            Транзакция 2
LOCK account 1
                        LOCK account 2
WAIT FOR account 2
                        WAIT FOR account 1

Both transactions are stuck (deadlock)

3. Race Condition

@Transactional(isolation = Isolation.READ_COMMITTED)
public void incrementCounter() {
    Counter counter = db.findById(1);  // counter = 10
    
    // Другая транзакция может прочитать 10 одновременно
    
    counter.setValue(counter.getValue() + 1);  // 10 + 1 = 11
    db.save(counter);
    
    // Другая транзакция также сохранит 11
    // Вместо 12 будет 11 (потеря инкремента)
}

Spring использование уровней изоляции

@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalOperation() {
    // Используй SERIALIZABLE только для критичных операций
    // Цена высокая в производительности
}

@Transactional(isolation = Isolation.READ_COMMITTED)
public void normalRead() {
    // По умолчанию в большинстве БД
    // Хороший баланс между безопасностью и производительностью
}

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void strictConsistency() {
    // Для операций требующих точности
}

Когда выбрать какой уровень

READ UNCOMMITTED: Почти никогда (опасно)

READ COMMITTED:

  • Аналитические запросы
  • Чтение неважной информации

REPEATABLE READ:

  • MySQL по умолчанию
  • Когда нужна согласованность

SERIALIZABLE:

  • Финансовые операции
  • Критичные бизнес-логики
  • Когда потери данных недопустимы

Вывод

Да, транзакции ВЛИЯЮТ друг на друга, но это влияние можно контролировать через:

  1. Уровень изоляции — чем выше, тем меньше влияние
  2. Блокировки — SELECT FOR UPDATE, SERIALIZABLE
  3. Оптимистичные блокировки — версионирование (@Version)
  4. Пессимистичные блокировки — явная блокировка

Выбор уровня изоляции — это вечный компромисс между безопасностью данных и производительностью.