Всем привет! В прошлый раз мы разобрали прокси и исключения. Сегодня поговорим о том, что происходит с транзакцией, когда один сервис вызывает другой.



Представим классическую связку: создание заказа.

@Service
public class OrderService {

private final PaymentService paymentService;

public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}

@Transactional
public void placeOrder(OrderDto dto) {
orderRepository.save(dto.toEntity());

// Вызываем метод другого бина (PaymentService)
paymentService.chargeMoney(dto.getUserId(), dto.getAmount());

log.info("Заказ успешно создан");
}
}

@Service
public class PaymentService {

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void chargeMoney(Long userId, BigDecimal amount) {
paymentRepository.withdraw(userId, amount);

if (someBusinessCheckFailed()) {
throw new RuntimeException("Антифрод система заблокировала платеж");
}
}
}


Кажется логичным: если антифрод сработал в chargeMoney(), то вся транзакция должна откатиться. Заказ не должен создаться, ведь деньги списать не удалось.

Но по умолчанию Spring сделает commit для заказа.

В базе останется запись об заказе, а пользователь получит уведомление «Оплата отклонена». Деньги при этом действительно не спишутся (так как у платежа был свой REQUIRES_NEW), но целостность данных нарушена — заказ висит без оплаты.

Почему так?
Spring управляет границами транзакций строго на входе в Proxy-объект.

Когда контроллер зовет orderService.placeOrder():
1. Срабатывает Proxy класса OrderService.
2. Открывается Транзакция №1 (T1).
3. Выполняется код внутри вашего объекта OrderService.
4. Мы заходим в paymentService.chargeMoney(). Это вызов другого бина через внедренную зависимость (@Autowired / конструктор).
5. Срабатывает Proxy класса PaymentService.
6. Текущая T1 ставится на паузу (suspend).
7. Открывается совершенно независимая Транзакция №2 (T2).
8. Если внутри T2 падает RuntimeException, отрабатывает только rollback для T2.
9. Управление возвращается в OrderService. Прокси PaymentService отработал и умер вместе со своей транзакцией.
10. Код OrderService продолжает выполнение до конца метода.
11. Достигается закрывающая скобка метода placeOrder(). Прокси OrderService видит штатный выход из метода и делает commit для T1.

Прокси первого сервиса ничего не знает о падении внутреннего прокси второго. Для него всё прошло успешно.

Как починить?
Нужно научить родительскую транзакцию реагировать на ошибки дочерней. Есть два основных способа:

1. Параметр rollbackFor. Самый простой вариант. Мы явно говорим родителю: «Если мой коллега упал вот с такой ошибкой — убей и мою транзакцию тоже».


@Transactional(rollbackFor = PaymentException.class)
public void placeOrder(OrderDto dto) {
// ... логика ...
paymentService.chargeMoney(...); // Кинет PaymentException
}


Важное условие: исключение должно вылететь наружу из chargeMoney непойманным, дойти до границы placeOrder и быть перечисленным в rollbackFor.

2. Глобальный аспект @AfterThrowing. Если таких сервисов много, прописывать rollbackFor везде — плохая идея. Можно написать один общий Aspect, который будет слушать падение определенных методов и программно помечать текущую транзакцию под откат:


@Aspect
@Component
public class TransactionRollbackAspect {

@AfterThrowing(
pointcut = "execution(* com.example..PaymentService.*(..))",
throwing = "ex")
public void markRollbackOnPaymentError(RuntimeException ex) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}


Теперь любая ошибка в любом методе PaymentService автоматически заставит откатиться любую внешнюю транзакцию, откуда бы его ни вызвали.
Просмотры: 43
Открыть в MAX Сообщить о неточности

Репосты публикации

Публикация канала

Сообщить о неточности

Опишите, что нужно исправить. Если есть источник, добавьте ссылку.

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти