АрхитектураМикросервисы и интеграцииАрхитектор распределённых систем

Запись бизнес изменения уже подтверждена, но событие иногда не появляется в брокере. Какой механизм устраня...

Запись бизнес-изменения уже подтверждена, но событие иногда не появляется в брокере. Какой механизм устраняет это расхождение?

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

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

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

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

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

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

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

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

Пусть сервис резервирует товар и после этого должен опубликовать событие о резервировании. Между фиксацией резерва и публикацией могут произойти сетевой сбой, перезапуск процесса, недоступность брокера или тайм-аут.

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

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

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

Отдельный процесс-издатель читает необработанные записи outbox и публикует их в брокер. После подтверждения публикации он отмечает запись как отправленную. Если процесс завершился после публикации, но до отметки, запись будет отправлена повторно — это нормальное следствие модели at-least-once delivery.

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

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

Альтернативой может быть Change Data Capture, когда изменения базы автоматически преобразуются в события. Это уменьшает объём прикладного кода, но сильнее связывает интеграционный контракт со структурой базы и требует отдельной инфраструктуры. Прямой вызов брокера из бизнес-транзакции проще на вид, но оставляет окно несогласованности и потому не даёт той же гарантии.

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

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

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

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

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

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

Нет. Если доставщик опубликовал событие, но упал до фиксации отметки об отправке, он повторит публикацию. Надёжная архитектура обычно принимает модель at-least-once и делает обработку потребителя идемпотентной.

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

  1. Почему нельзя пометить запись outbox отправленной до публикации?

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

Этот порядок допускает дубликаты, но не создаёт окно безвозвратной потери внутри самого механизма outbox. Дубликаты контролируются на стороне потребителя, а не ценой отказа от повторной доставки.

  1. Решает ли outbox согласованность между несколькими базами данных?

Нет. Он атомарно связывает только изменения, выполняемые в одной локальной транзакции источника. Если одна бизнес-операция должна изменить несколько сервисов, каждый сервис применяет свою локальную транзакцию, а общая координация строится через события и процесс, например с компенсациями.

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