← Назад к вопросам
Что будет если в рамках транзакции произойдет ошибка?
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 - Всегда передавайте исключения выше, не глушите их
- Задавайте явное поведение через аннотации