Как согласовать запись заказа в БД с публикацией события при сбое между этими действиями?
Используют паттерн Transactional Outbox: запись заказа и запись события в специальную таблицу выполняются в одной транзакции базы данных. Отдельный доставщик затем публикует события из этой таблицы в брокер, поэтому сбой между бизнес-изменением и отправкой не приводит к потере намерения уведомить другие системы.
Этот подход не обеспечивает автоматическую доставку ровно один раз. Доставщик может повторно отправить событие, поэтому потребители должны обрабатывать его идемпотентно, а событие — иметь устойчивый идентификатор.
В распределённых системах бизнес-сервис, база данных и брокер сообщений обычно являются отдельными компонентами. Одна локальная транзакция базы данных не может атомарно включить публикацию сообщения в удалённый брокер.
Без специального подхода разработчики часто сначала изменяют БД, а затем отправляют событие. При сбое между этими действиями данные уже изменены, но другие сервисы об этом не узнают. Обратный порядок создаёт противоположную проблему: событие может быть опубликовано для операции, которая позже не зафиксировалась в БД.
Transactional Outbox появился как практичная замена распределённым транзакциям в сценариях, где важны надёжность и приемлемая задержка, но не требуется синхронная атомарность между несколькими системами.
При создании заказа нужно сохранить его состояние и передать событие, например для запуска резервирования товара. Возможны сбои приложения, сети, базы данных или брокера после выполнения только одной части операции.
Если событие потеряно, резервирование не запустится, а заказ останется в состоянии, которое другие системы считают неполным. Если событие отправлено дважды, потребитель может повторно зарезервировать товар, создать дубликат уведомления или повторно выполнить другой побочный эффект.
Неверно выбранная модель также создаёт операционные риски: бесконечный рост очереди неотправленных событий, нарушение порядка событий одной сущности и отсутствие возможности понять, почему доставка остановилась.
В рамках одной транзакции БД сервис сохраняет заказ и добавляет в таблицу outbox запись с идентификатором события, типом, версией схемы, полезной нагрузкой, временем создания и статусом доставки. Если транзакция зафиксирована, сохранены оба изменения; если отклонена — не сохранено ни одно.
Отдельный процесс или компонент доставки периодически читает необработанные записи, публикует события в брокер и отмечает успешную доставку. Если процесс завершился после публикации, но до отметки в outbox, он повторит отправку при следующем запуске. Поэтому модель доставки обычно является как минимум один раз.
Потребитель должен проверять идентификатор события или бизнес-ключ операции и безопасно повторять обработку. Для этого применяют таблицу обработанных событий, уникальные ограничения, идемпотентные операции или комбинацию этих механизмов.
События следует версионировать, а их порядок явно определять. Простая сортировка по времени записи не всегда достаточна: для порядка изменений одной сущности может понадобиться номер последовательности, а параллельная обработка разных сущностей обычно допустима.
Паттерн увеличивает задержку: событие публикуется не в момент фиксации транзакции, а после чтения outbox доставщиком. Он также добавляет таблицу, фоновый процесс, мониторинг, повторные попытки, очистку старых записей и обработку неисправимых ошибок через отдельную очередь.
Удаление записи после успешной доставки опасно, если аудит или повторная публикация ещё нужны. Срок хранения выбирают исходя из требований к восстановлению, отладки и повторной обработке; удаление должно быть безопасным и наблюдаемым.
Интернет-магазин сохранял заказ в базе, а затем напрямую отправлял сообщение сервису резервирования. При кратковременной недоступности брокера заказ создавался успешно, но событие терялось. Повторная отправка всего запроса была неприемлема: она могла создать второй заказ.
Рассматривались три варианта. Прямая публикация была простой, но не устраняла окно сбоя. Распределённая транзакция давала более сильную атомарность, однако требовала поддержки согласованного протокола всеми участниками и усложняла эксплуатацию. Периодическая сверка заказов с резервами могла обнаруживать расхождения, но создавала задержку и сложную логику поиска пропусков.
Выбрали Transactional Outbox. Заказ и событие стали фиксироваться одной транзакцией, доставщик получил повторные попытки с ограничением частоты, а сервис резервирования — защиту от повторной обработки по идентификатору события.
В результате временная недоступность брокера перестала приводить к потере события: оно оставалось в outbox до успешной доставки. Цена решения — дополнительная задержка, инфраструктура доставки и необходимость контролировать размер таблицы и возраст необработанных сообщений.
1. Гарантирует ли Transactional Outbox доставку события ровно один раз?
Нет. Если доставщик успел передать сообщение брокеру, но не успел зафиксировать отметку об успешной отправке, событие будет опубликовано повторно. Поэтому outbox обычно даёт семантику at-least-once, а не exactly-once.
Ровно одно выполнение бизнес-эффекта нужно обеспечивать на стороне потребителя: через идемпотентную обработку, уникальный идентификатор операции или атомарную фиксацию результата вместе с отметкой о принятом событии. Даже поддержка exactly-once на уровне транспорта не отменяет необходимости учитывать повторы при внешних побочных эффектах.
2. Почему нельзя считать запись в outbox достаточной без контроля доставщика?
Запись гарантирует сохранение намерения, но не гарантирует его фактическую публикацию. Доставщик может быть остановлен, потерять доступ к брокеру, постоянно получать некорректное событие или блокироваться на одной неисправной записи.
Нужны мониторинг возраста необработанных сообщений, число повторных попыток, метрики задержки, журнал ошибок и понятная процедура повторной обработки. Неисправимые сообщения обычно изолируют, чтобы они не блокировали последующие события.
3. Когда outbox не решает проблему согласованности?
Он согласует БД сервиса с публикацией события, но не делает атомарными изменения в БД другого сервиса. Получатель может временно не обработать событие, обработать его с ошибкой или применить изменения в другом порядке.
Если требуется согласованное многошаговое бизнес-изменение, одной публикации событий недостаточно. Нужны модель саги, компенсационные действия, явные состояния процесса и правила восстановления. Outbox в такой схеме остаётся надёжным механизмом передачи локального факта, но не заменяет управление распределённым процессом.