В очереди сообщений обработчик завершил бизнес-операцию, но процесс упал до подтверждения сообщения. Какую семантику доставки следует ожидать?
Следует ожидать семантику at-least-once: сообщение может быть доставлено повторно. Брокер не видит подтверждения и обычно считает обработку незавершённой, поэтому повторяет доставку после тайм-аута, переподключения или перезапуска потребителя.
Обработчик должен быть идемпотентным: повторная обработка одного логического сообщения не должна повторно создавать бизнес-эффект. Подтверждать сообщение следует только после надёжной фиксации результата обработки.
Очереди сообщений применяются для развязки производителей и потребителей, сглаживания нагрузки и переживания временной недоступности сервисов. Для этого брокеру требуется механизм подтверждения: потребитель сообщает, что сообщение принято и обработано.
Если подтверждение не получено, безопаснее повторить доставку, чем окончательно потерять сообщение. Так сформировалась практическая модель at-least-once, которая предпочитает возможные дубликаты риску потери данных.
Между выполнением бизнес-операции и отправкой подтверждения существует окно отказа. Например, обработчик записал заказ в базу, но завершился до подтверждения сообщения.
После восстановления брокер повторно передаст сообщение. Без защиты это может привести к двойному списанию, повторному созданию заказа, дублирующему уведомлению или повторной публикации события.
Раннее подтверждение устраняет дубликаты, но создаёт обратную опасность: если процесс подтвердил сообщение до выполнения операции и затем упал, сообщение может быть потеряно. Поэтому выбор момента подтверждения — это компромисс между потерей и повторной обработкой.
При модели at-least-once последовательность обычно такова: потребитель получает сообщение, выполняет операцию, надёжно фиксирует её результат и только после этого отправляет подтверждение брокеру. Сбой до подтверждения вызывает повторную доставку, поэтому повторная обработка должна распознаваться по стабильному идентификатору сообщения или операции.
Для защиты используют идемпотентность и дедупликацию. Потребитель хранит идентификатор уже применённой операции или применяет изменение с уникальным ограничением в той же транзакции, что и бизнес-данные. Повторная доставка тогда приводит к проверяемому отсутствию нового эффекта, после чего сообщение можно подтвердить.
Это не означает, что брокер гарантирует глобальную семантику exactly-once. Если обработчик изменяет внешнюю систему, например платёжный шлюз, локальная транзакция базы данных не делает внешний вызов атомарным с записью результата. В таком случае нужны поддержка идемпотентных ключей внешней операции, устойчивый журнал намерений или согласованный процесс с возможностью повторной проверки результата.
У дедупликации есть цена: требуется долговечное хранилище идентификаторов, политика его очистки и корректный выбор области уникальности. Слишком раннее удаление записи о сообщении позволит повторить эффект, а бесконечное хранение увеличит объём данных. Для операций, которые нельзя безопасно повторять, полезно моделировать переходы состояния так, чтобы повторный запрос был допустим только из ожидаемого состояния.
Сервис обработки заказов получает сообщение о необходимости создать отгрузку. Он записывает отгрузку в базу, но падает перед подтверждением сообщения. При повторной доставке наивный обработчик создаёт вторую отгрузку для того же заказа.
Рассматривались три варианта. Подтверждать сообщение до записи в базу быстро и исключает дубликаты, но допускает потерю отгрузки. Пытаться добиться exactly-once только настройками брокера недостаточно: брокер не контролирует транзакцию базы данных. Оставить подтверждение после записи и не менять обработчик проще всего, но дубликаты останутся.
Выбран вариант at-least-once с идемпотентным обработчиком: идентификатор заказа используется как ключ уникальности, а запись отгрузки и фиксация обработанного идентификатора выполняются атомарно в одной транзакции базы данных. После успешной транзакции отправляется подтверждение. При повторной доставке уникальность предотвращает вторую отгрузку, а сбои приводят лишь к повторной проверке состояния.
Нет, это меняет риск, но не решает задачу. При падении после подтверждения и до бизнес-операции брокер уже не обязан доставлять сообщение повторно, поэтому возникает потеря обработки. Такой режим соответствует приближённой семантике at-most-once и подходит только тогда, когда потеря допустима или операция будет восстановлена другим механизмом.
Только если все повторные доставки одной логической операции используют один и тот же стабильный идентификатор. Если производитель создаёт новый идентификатор при каждом повторе, потребитель не сможет распознать дубликат. Кроме того, ключ должен соответствовать бизнес-операции: разные сообщения могут требовать разных эффектов, а одинаковый заказ не всегда означает одинаковую допустимую операцию.
Проверка «идентификатор ещё не обработан» без атомарной фиксации создаёт гонку: оба экземпляра могут пройти проверку и выполнить эффект. Запись результата и регистрация идентификатора должны защищаться уникальным ограничением, транзакцией или другой атомарной операцией. При конфликте один экземпляр фиксирует результат, а второй распознаёт повтор и безопасно подтверждает сообщение.