intDataBlog
← ЖурналМаршруты обучения ↗

Transactional outbox: как не потерять событие после записи в базу

Одна локальная транзакция для бизнес-изменения и события, доставка с повторами и проверка потребителя.

Сервис сохранил заказ и должен сообщить об этом складу. Между двумя действиями упал процесс. В базе заказ есть, а склад о нём не знает. Если поменять действия местами, появится обратная проблема: сообщение ушло, но транзакция заказа откатилась.

Transactional outbox переносит намерение отправить событие в ту же базу, где происходит бизнес-изменение. Документация Debezium описывает outbox как способ согласовать внутреннее состояние сервиса с событиями для других систем. Debezium Outbox Event Router 2.7

Что именно попадает в транзакцию

Представим учебный сервис бронирования. В одной транзакции он создаёт бронь и строку события. Сам сетевой вызов брокера в эту транзакцию не входит.

BEGIN;

INSERT INTO reservations (id, room_id, status)
VALUES ('r-demo-17', 'room-demo-2', 'confirmed');

INSERT INTO outbox (id, aggregate_id, event_type, payload)
VALUES (
  'event-demo-91',
  'r-demo-17',
  'reservation.confirmed.v1',
  '{"reservation_id":"r-demo-17","room_id":"room-demo-2"}'
);

COMMIT;

Названия и схема здесь учебные, это не конфигурация Debezium. Структура конкретной outbox-таблицы зависит от выбранного доставщика. Важное свойство примера: либо обе записи фиксируются, либо не фиксируется ни одна. Из этого не следует, что получатель уже увидел событие.

После фиксации отдельный процесс читает outbox и отправляет сообщения. Это может быть опрос таблицы или чтение изменений через CDC. В обоих случаях нужно заранее ответить на вопросы: кто забирает запись, как несколько исполнителей делят работу и по какому признаку запись можно считать доставленной.

Почему дубль всё равно возможен

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

Стабильный идентификатор события позволяет получателю узнавать такой повтор. В Debezium у outbox-события предусмотрен отдельный ID, а ключ агрегата используется при маршрутизации сообщений. Это полезные элементы контракта, но не автоматическая гарантия однократного бизнес-эффекта у потребителя. Описание полей outbox

Для учебной брони получатель может в одной своей транзакции записать ID обработанного события и обновить локальную проекцию. Если эти два действия разнести, окно потери появится уже на принимающей стороне. Внешний эффект вроде отправки письма потребует отдельного решения: запись в локальной таблице сама по себе не делает почтовый API идемпотентным.

Порядок — отдельная задача

События «бронь подтверждена» и «бронь отменена» относятся к одному объекту. Параллельная доставка и повторная обработка не должны вернуть его в старое состояние. Для этого в контракте можно использовать версию агрегата и правило применения изменений. Конкретное решение зависит от того, передаётся полное состояние или последовательность операций.

В учебном варианте получатель принимает состояние только с большей версией. Пропуск версии тогда нужно обнаружить отдельно: возможно, потерянное событие содержало важный самостоятельный эффект. Поэтому «брать самое новое» подходит не для любого процесса.

Проверять границы падения

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

Ожидания формулируются через состояние: у подтверждённой брони есть событие, у откатившейся его нет, повтор не создаёт второй бизнес-эффект, отставание доставщика заметно. Возраст самой старой необработанной записи часто полезнее одного счётчика ошибок: очередь может расти, пока каждый отдельный запрос формально успешен.

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