Как сделать отправку invoice вне транзакции, чтобы не блокировать HTTP вызовы
Что проверяют
Видите ли вы проблему внешнего вызова внутри транзакции: пока идёт HTTP-запрос к платёжному или почтовому сервису, держатся соединение из пула и блокировки строк. Второй слой про согласованность: что будет, если инвойс ушёл, а транзакция откатилась. Следом спрашивают про гарантии доставки.
Как отвечать
Назовите проблему двойной записи: база и внешний сервис не участвуют в одной транзакции, и любой порядок действий оставляет окно рассогласования. Варианты по возрастанию надёжности:
- разнести шаги: метод-оркестратор без
@Transactionalсохраняет счёт в одной короткой транзакции, после коммита делает HTTP-вызов, затем второй транзакцией пишет статус. Соединение не держится во время сети, но при падении между шагами счёт останется неотправленным; - после коммита: публикуем событие и ловим его в
@TransactionalEventListener(phase = AFTER_COMMIT)вместе с@Asyncна своём пуле. Инвойс не уйдёт при откате, но при рестарте сервиса событие теряется; - transactional outbox: в той же транзакции, что и счёт, пишем строку в таблицу
outbox. Отдельный процесс, опрос по расписанию или CDC через Debezium, забирает записи и отправляет в Kafka или сразу во внешний сервис с повторами. Это доставка «хотя бы один раз», поэтому получатель должен быть идемпотентным: ключ идемпотентности по номеру счёта; - ответ клиенту: если ждать долго, эндпоинт возвращает
202 Accepted, а статус клиент опрашивает позже.
Нужны таймауты на HTTP-клиенте (RestClient, WebClient) и повторы с экспоненциальной задержкой. Circuit breaker защищает от лавины запросов в лежащий сервис, но доставку не гарантирует.
Где спотыкаются
Предлагают CompletableFuture.runAsync без своего пула. Задача уходит в общий ForkJoinPool.commonPool(), который не рассчитан на блокирующий ввод-вывод, исключение теряется, а при рестарте теряется и сама отправка.
Вызывают @Async метод изнутри транзакции и ждут, что он увидит только что сохранённый счёт. Новый поток не участвует в транзакции и незакоммиченных данных не видит.
Шлют сообщение в брокер внутри транзакции и считают задачу решённой. Это та же двойная запись: сообщение уйдёт, а коммит может не случиться.