Практическая ситуация: сервис сохраняет заказ в транзакционной базе, затем отдельно публикует событие для других систем. Как устранить риск расхождения между записью заказа и публикацией события?
Используйте паттерн Transactional Outbox: запись заказа и связанного события выполняется в одной транзакции базы данных. Отдельный доставщик читает сохранённые события из outbox и публикует их в брокер, поэтому сбой между операциями не приводит к потере события.
Надёжная доставка обычно имеет семантику at-least-once: событие может быть опубликовано повторно. Поэтому потребители должны обрабатывать его повторно безопасно, например по уникальному идентификатору события.
Распределённые системы часто должны одновременно изменить локальное состояние и уведомить другие компоненты. Простая последовательность из записи в базе и публикации в брокер создаёт две независимые операции, между которыми возможен сбой.
Паттерн Transactional Outbox появился как практический способ связать изменение бизнес-состояния с намерением отправить событие без распределённой транзакции между базой данных и брокером. Он использует уже доступную атомарность транзакций одной базы.
Если сначала записать заказ, а затем публиковать событие, падение процесса между шагами оставит заказ в базе без уведомления потребителей. Если поменять порядок, публикация может завершиться успешно, а транзакция записи заказа — откатиться.
Такие расхождения приводят к неполному поисковому индексу, неверным остаткам, пропущенным уведомлениям или состоянию downstream-систем, которое нельзя объяснить только данными основной базы. Повторная отправка после каждого сбоя без фиксированного журнала событий также может породить дубликаты.
В одной транзакции сохраняются бизнес-сущность и строка в таблице outbox. В строке обычно находятся уникальный идентификатор события, тип события, идентификатор агрегата, полезная нагрузка, время создания и статус либо сведения о попытках доставки.
После фиксации транзакции отдельный relay или воркер выбирает опубликованные в outbox события, передаёт их брокеру и отмечает результат. Если процесс завершился после публикации, но до отметки, событие будет отправлено повторно при следующей попытке. Это нормальная цена отказоустойчивой схемы без общей транзакции с брокером.
Потребитель должен использовать идентификатор события или бизнес-ключ для дедупликации. Если важен порядок событий одного агрегата, его нужно обеспечивать отдельно: например, последовательным номером версии и проверкой ожидаемой версии на стороне потребителя; сам outbox автоматически не решает глобальное упорядочивание между всеми сущностями.
Outbox необходимо обслуживать: индексировать выборку необработанных записей, ограничивать размер полезной нагрузки, повторять временно неуспешные публикации с контролем нагрузки и удалять либо архивировать подтверждённые события. Для больших объёмов применяют партиционирование или CDC-экстракцию outbox, но при этом сохраняют атомарность записи бизнес-данных и события.
Паттерн не даёт мгновенной публикации и не обеспечивает exactly-once end-to-end. Он также не заменяет согласование схемы событий, мониторинг задержки доставки, контроль переполнения outbox и процедуру исправления неисправимых сообщений.
Платёжный сервис сохранял успешный платёж в базе, после чего отправлял событие в брокер для сервиса чеков. Во время кратковременных сбоев брокера часть платежей уже была зафиксирована, но события не появлялись, поэтому чеки не формировались.
Рассматривались два варианта. Распределённая транзакция между базой и брокером давала бы более сильную координацию, но усложняла инфраструктуру, увеличивала связанность компонентов и зависела от поддержки единого протокола всеми участниками. Периодическая сверка платежей с данными чеков была проще, но обнаруживала проблему с задержкой и требовала отдельной логики восстановления.
Выбрали outbox: платёж и событие стали фиксироваться одной транзакцией, а relay публиковал события с повторными попытками. Потребитель чеков хранил обработанные идентификаторы событий, поэтому повторная доставка не создавала повторный чек. В результате потери уведомлений из-за сбоя между записью и публикацией исчезли, а задержка стала контролируемым параметром мониторинга.
Что произойдёт, если relay упадёт после успешной публикации, но до отметки события как отправленного?
Событие будет опубликовано повторно. Это неизбежный сценарий при раздельных операциях публикации и фиксации статуса, поэтому архитектура должна рассчитывать на at-least-once delivery. Потребитель обязан сделать обработку идемпотентной или вести дедупликацию по стабильному идентификатору события.
Почему нельзя считать сам outbox гарантией доставки в брокер?
Outbox гарантирует атомарное сохранение намерения отправить событие вместе с бизнес-изменением, но не гарантирует успешную работу relay, доступность брокера или обработку события потребителем. Нужны мониторинг возраста необработанных записей, повторные попытки, очередь неисправимых сообщений и операционная процедура восстановления.
Когда outbox может стать узким местом?
При высокой частоте событий таблица outbox превращается в горячий журнал: растёт конкуренция за выборку, увеличивается объём индексов и усложняется очистка. Решение выбирают по нагрузке: пакетная отправка, партиционирование, отдельные индексы для необработанных записей, архивирование или CDC outbox. При этом нельзя жертвовать главным свойством паттерна — событие должно записываться в той же транзакции, что и изменение бизнес-состояния.