Спецпроекты

EventRelay

Платёж и событие сохраняются одной транзакцией и уходят в Kafka через outbox. Получатель помнит обработанные события, поэтому повторная доставка ничего не ломает.

Тип
Платёжный сервис
Стек
Java · PostgreSQL · Kafka
Код
Открыт на GitHub
Год
2026

Почта с одним конвертом

Платёж и конверт события ложатся в ящик одним движением. Каретка отправляет конверт по трубе, а у получателя наколка с номерами: повторный конверт падает в лоток «уже было».

DEAD
Конверт в пути: за ним и следитеЯщик outbox: платёж и конверт ложатся одной транзакциейНаколка получателя: известный номер уходит в лоток «уже было»Модель EventRelay, платёжного сервиса на Java с PostgreSQL и Kafka. Платёж и строка outbox записываются в базу одной транзакцией; у ящика поэтому одно движение на оба листа. Каретка разбирает строки и шлёт конверт в трубу (Kafka, acks=all). Если процесс упал после того, как Kafka приняла событие, но до отметки в базе, тот же конверт уйдёт ещё раз: доставка «хотя бы один раз», поэтому щелчок здесь роняет каретку сразу после отправки. При ошибке строка ждёт всё дольше, а после восьмой попытки получает статус DEAD и уходит на разбор оператору. Получатель записывает номер события вместе с обновлением проекции: известный номер падает в лоток «уже было», новый насаживается на наколку и сдвигает счёты. На столе один отправитель вместо нескольких, а паузы между попытками сжаты с секунд и минут до долей секунды.

Проблема

Процесс может упасть после того, как Kafka приняла событие, но до того, как база отметила его отправленным. Тогда событие уйдёт ещё раз. Значит, доставка будет «хотя бы один раз», и повтор должен быть безвредным.

Решение

Платёж и строка outbox записываются в PostgreSQL одной транзакцией. Несколько отправителей разбирают строки через FOR UPDATE SKIP LOCKED и шлют в Kafka с acks=all; при ошибке — повтор с паузой от секунды до пяти минут, после восьми попыток строка уходит на разбор оператору. Получатель записывает номер события вместе с обновлением проекции, поэтому повторная доставка ничего не меняет.

Технически

  • Transactional outbox: платёж и событие в одной транзакции.
  • Параллельные отправители без двойного захвата строк.
  • Экспоненциальные повторы и статус DEAD после восьми попыток.
  • Идемпотентный получатель: обработанные события хранятся с проекцией.
  • Битые сообщения после трёх попыток уходят в отдельную тему.
Код на GitHub

Обсудим ваш проект?

Обсудить проект