АрхитектураАрхитектура ПОАрхитектор программного обеспечения

Сервис одновременно изменяет состояние в базе данных и публикует событие через независимые операции. Как ус...

Сервис одновременно изменяет состояние в базе данных и публикует событие через независимые операции. Как устранить риск их рассинхронизации?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Используйте паттерн транзакционной исходящей очереди: изменение бизнес-состояния и запись события в специальную таблицу выполняются в одной локальной транзакции базы данных. Отдельный процесс затем надёжно публикует сохранённые события во внешний брокер и отмечает их обработанными.

Так устраняется основной риск двойной записи: состояние и намерение опубликовать событие либо фиксируются вместе, либо откатываются вместе. Полной сквозной гарантии однократной доставки это не даёт, поэтому потребители должны быть идемпотентными.

Исторический контекст

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

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

Постановка проблемы

Если сначала изменить базу данных, а затем отправить событие, сбой между операциями оставит сохранённое состояние без уведомления потребителей. Если сначала отправить событие, а затем изменить базу данных, потребитель может увидеть событие о факте, который не был зафиксирован.

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

Подробное решение

В рамках одной транзакции сервис изменяет свои бизнес-таблицы и добавляет запись в таблицу outbox. Запись содержит идентификатор события, тип, полезную нагрузку, время создания и состояние обработки. Если транзакция откатывается, в outbox не появляется событие; если фиксируется, событие гарантированно сохраняется вместе с изменением состояния.

Отдельный relay-процесс периодически выбирает необработанные записи, публикует их в брокер и помечает успешно отправленные записи. Для параллельной работы нескольких экземпляров нужны безопасный захват записей, блокировки или механизм резервирования, зависящий от возможностей используемой базы данных.

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

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

Альтернатива — распределённая транзакция между базой данных и брокером. Она может дать более сильную атомарность, но усложняет инфраструктуру, ухудшает отказоустойчивость и связывает компоненты протоколом координации. Поэтому outbox часто выбирают как компромисс между надёжностью, операционной сложностью и приемлемой eventual consistency.

Ситуация из практики

Сервис заказов после оформления сохранял заказ, а затем публиковал событие ЗаказОформлен. При аварии между этими действиями платёжный сервис не получал уведомление, хотя заказ уже отображался пользователю как созданный.

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

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

Что кандидаты часто упускают

  1. Гарантирует ли outbox ровно одну доставку события?

Нет. Обычно он обеспечивает сохранение события вместе с изменением состояния и доставку с семантикой как минимум один раз. Relay может повторить публикацию после неопределённого результата отправки, поэтому дубликаты неизбежно возможны. Ровно одна обработка достигается не транспортом, а идемпотентностью потребителя и контролем идентификатора события.

  1. Почему недостаточно просто повторять неудачную отправку из фонового процесса?

Повторные попытки помогают при временных сетевых сбоях, но не решают проблему потери намерения отправить событие. Если это намерение хранится только в памяти процесса, авария уничтожит его; если оно не связано транзакционно с бизнес-изменением, можно получить событие без соответствующего состояния или состояние без события. Outbox сначала делает намерение долговечным и атомарным относительно изменения данных, а уже затем применяет повторные попытки.

  1. Как обеспечить порядок событий для одного бизнес-объекта?

Нужно определить требуемую область порядка: глобальный порядок обычно дорог и не нужен. Для одного заказа можно сохранять последовательность или версию агрегата и публиковать события с этим номером, а потребитель — проверять ожидаемую версию и временно откладывать более поздние события. При нескольких независимых relay-процессах потребуется механизм партиционирования, упорядочивания или повторной обработки. Это повышает сложность и может снижать пропускную способность, поэтому порядок следует гарантировать только там, где он является бизнес-требованием.