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

Что будет если в рамках транзакции произойдет ошибка?

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

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

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

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

Обработка ошибок в транзакциях

Это критически важный вопрос для надёжной работы приложения. Поведение зависит от типа исключения и настройки транзакции. Разберу всё подробно.

По умолчанию: rollback при RuntimeException

@Service
public class PaymentService {
    @Autowired
    private PaymentRepository repository;
    
    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        Payment payment = new Payment();
        payment.setOrderId(orderId);
        payment.setAmount(amount);
        
        repository.save(payment);  // INSERT
        
        // Если тут произойдёт исключение...
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Invalid amount");
            // RuntimeException -> ROLLBACK
        }
    }
}

Результат: Payment не будет сохранён (откат всей транзакции)

Checked Exceptions — НЕ откатываются

@Service
public class FileService {
    @Autowired
    private UserRepository repository;
    
    @Transactional
    public void saveUserWithFile(User user) throws IOException {
        repository.save(user);  // INSERT успешен
        
        // Checked exception НЕ откатывает транзакцию!
        File file = new File("/path/to/file");
        file.createNewFile();  // IOException
        
        // User остался в БД несмотря на ошибку файла
    }
}

Проблема: Бизнес-логика нарушена: User есть, но файл не создан.

Решение:

@Transactional
public void saveUserWithFile(User user) throws IOException {
    try {
        repository.save(user);
        File file = new File("/path/to/file");
        file.createNewFile();
    } catch (IOException e) {
        // Бросаем RuntimeException для откката
        throw new RuntimeException("Failed to create file", e);
    }
}

// ИЛИ используем rollbackFor
@Transactional(rollbackFor = IOException.class)
public void saveUserWithFile(User user) throws IOException {
    repository.save(user);
    File file = new File("/path/to/file");
    file.createNewFile();
}

Управление откатом транзакции

1. rollbackFor — явно указываем, какие исключения откатывают

@Transactional(rollbackFor = Exception.class)
public void processOrder(Order order) throws Exception {
    repository.save(order);
    // ВСЕ Exception-y откатят транзакцию
    someMethodThatThrowsCheckedException();
}

2. noRollbackFor — указываем, какие НЕ откатывают

@Transactional(noRollbackFor = ValidationException.class)
public void saveUser(User user) {
    repository.save(user);
    
    try {
        validateEmail(user.getEmail());
    } catch (ValidationException e) {
        // Бросаем исключение, но транзакция коммитится
        throw e;
    }
}

3. Комбинация

@Transactional(
    rollbackFor = {IOException.class, SQLException.class},
    noRollbackFor = ValidationException.class
)
public void complexOperation() throws Exception {
    // IOException и SQLException откатят
    // ValidationException НЕ откатит
}

Типы исключений

Unchecked (Runtime) — откатывают по умолчанию:

@Transactional
public void example() {
    repository.save(entity);
    throw new NullPointerException();        // ROLLBACK
    throw new IllegalArgumentException();    // ROLLBACK
    throw new IllegalStateException();       // ROLLBACK
}

Checked (Throwable) — НЕ откатывают по умолчанию:

@Transactional
public void example() throws IOException {
    repository.save(entity);
    throw new IOException();    // COMMIT (!)  — опасно!
    throw new SQLException();   // COMMIT (!) — опасно!
}

Практический пример: обработка платежа

@Service
public class PaymentProcessingService {
    @Autowired
    private PaymentRepository paymentRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private PaymentGateway paymentGateway;
    
    @Transactional(rollbackFor = PaymentException.class)
    public void processPayment(Order order) throws PaymentException {
        // 1. Сохраняем платёж в статусе PENDING
        Payment payment = new Payment();
        payment.setOrder(order);
        payment.setStatus(PaymentStatus.PENDING);
        payment.setAmount(order.getTotal());
        paymentRepository.save(payment);
        
        try {
            // 2. Обращаемся к платёжной системе
            String transactionId = paymentGateway.charge(
                order.getCustomer().getCardToken(),
                order.getTotal()
            );
            
            // 3. Обновляем статус
            payment.setStatus(PaymentStatus.SUCCESS);
            payment.setTransactionId(transactionId);
            paymentRepository.save(payment);
            
            // 4. Обновляем заказ
            order.setStatus(OrderStatus.PAID);
            orderRepository.save(order);
            
        } catch (PaymentGatewayException e) {
            // Платёж не прошёл — откатываем ВСЮ транзакцию
            // Платёж остаётся PENDING (или удаляется вообще)
            payment.setStatus(PaymentStatus.FAILED);
            payment.setErrorMessage(e.getMessage());
            paymentRepository.save(payment);
            throw new PaymentException("Payment failed", e);
        }
    }
}

Nested transactions (сложный случай)

Проблема: одна ошибка откатывает всю цепочку

@Service
public class OrderService {
    @Autowired
    private PaymentService paymentService;
    
    @Transactional
    public void createOrder(Order order) {
        // ... создание заказа
        
        // Вызов другого @Transactional сервиса
        try {
            paymentService.processPayment(order);
        } catch (PaymentException e) {
            // Что произойдёт?
            // Если paymentService бросит RuntimeException,
            // ВЕСЬ заказ будет откачен!
        }
    }
}

@Service
public class PaymentService {
    @Transactional
    public void processPayment(Order order) throws PaymentException {
        // ...
        throw new RuntimeException();
        // Это откатит И этот метод, И createOrder
    }
}

Решение: PROPAGATION.REQUIRES_NEW

@Service
public class PaymentService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processPayment(Order order) throws PaymentException {
        // Новая независимая транзакция
        // Откат здесь НЕ повлияет на createOrder
    }
}

Теперь при исключении:

  • Платёж откатывается независимо
  • Заказ может быть сохранён (зависит от обработки ошибки в OrderService)

Различные Propagation уровни

// 1. REQUIRED (по умолчанию) — включить в существующую транзакцию
@Transactional(propagation = Propagation.REQUIRED)
public void method() { }

// 2. REQUIRES_NEW — новая транзакция, независимо от текущей
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void method() { }

// 3. NESTED — сохранённая точка (savepoint) внутри транзакции
@Transactional(propagation = Propagation.NESTED)
public void method() { }  // Можно откатить только эту часть

// 4. SUPPORTS — если есть транзакция, присоединиться, иначе no-op
@Transactional(propagation = Propagation.SUPPORTS)
public void method() { }

Best Practices

1. Не ловите исключения без причины

// Плохо
@Transactional
public void save() {
    try {
        repository.save(entity);
    } catch (Exception e) {
        System.out.println("Error!");
        // Исключение проглочено — транзакция коммитится!
    }
}

// Хорошо
@Transactional
public void save() {
    repository.save(entity);
    // Исключение пройдёт выше, откатит транзакцию
}

2. Задавайте timeout

@Transactional(timeout = 30)  // 30 секунд
public void longRunningOperation() {
    // Если операция дольше 30 сек, откатится с TimeoutException
}

3. Read-only для оптимизации

@Transactional(readOnly = true)
public User getUser(Long id) {
    // Не нужна полная мощь транзакции для чтения
    // Это может улучшить производительность
    return repository.findById(id).orElse(null);
}

4. Правильная изоляция

@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoney(Account from, Account to) {
    // Защита от dirty reads
    // Но можны non-repeatable reads
}

Итог

  • RuntimeException откатывает транзакцию по умолчанию
  • Checked Exception НЕ откатывает — используй rollbackFor
  • Используй noRollbackFor для исключений, которые НЕ должны откатывать
  • Для независимых операций используй PROPAGATION.REQUIRES_NEW
  • Всегда передавайте исключения выше, не глушите их
  • Задавайте явное поведение через аннотации
Что будет если в рамках транзакции произойдет ошибка? | PrepBro