Очереди обычно гарантируют доставку как минимум один раз, поэтому повтор сообщения — нормальная ситуация. Если consumer создает новый заказ при каждом получении, дубли неизбежны при timeout, reconnect или падении до подтверждения.

Найдите устойчивый идентификатор бизнес-операции и сделайте создание заказа идемпотентным на уровне базы. После этого настройте ack и retries, а не наоборот.

Коротко: что сделать

  • Определить уникальный operation_id
  • Найти повторяющиеся message_id в логах
  • Проверить момент ack относительно транзакции
  • Проверить уникальные ограничения базы
  • Проверить retry и dead-letter политику

Почему возникает проблема

Consumer может завершить запись в базе, но упасть до ack. Брокер честно доставит сообщение снова, и приложение должно распознать повтор.

  • У сообщения нет idempotency key
  • Уникальность проверяется запросом без индекса
  • Ack отправляется слишком рано или слишком поздно без схемы
  • Транзакция заказа и запись обработки разделены
  • Producer повторно публикует событие после timeout
  • Несколько consumer одновременно обрабатывают один бизнес-ключ

Пошаговая диагностика

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

  • Сгруппировать дубли по внешнему operation_id
  • Сопоставить время создания с delivery count
  • Проверить consumer log до и после commit
  • Смоделировать падение после INSERT до ack
  • Проверить конкурирующую обработку
  • Проверить DLQ и ручной replay

Как исправить

Защита должна работать атомарно: либо первая операция создает заказ, либо повтор получает уже созданный результат.

  • Добавить idempotency key в контракт события
  • Создать уникальный индекс по бизнес-операции
  • В одной транзакции фиксировать заказ и факт обработки
  • Обрабатывать duplicate key как успешный повтор
  • Использовать outbox/inbox для межсервисной доставки
  • Настроить ограниченные retries и DLQ

Как проверить результат

  • Отправить одно событие несколько раз
  • Остановить consumer между commit и ack
  • Запустить несколько consumer параллельно
  • Повторить событие из DLQ
  • Убедиться, что создается один заказ и один платежный сценарий

Как не допустить повторения

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

  • Документировать ключ операции
  • Добавить chaos-тесты повторной доставки
  • Мониторить duplicate key и DLQ
  • Не удалять inbox-записи раньше допустимого окна повторов

Чего не стоит делать

  • Не надеяться на exactly once без защиты базы
  • Не искать дубль только SELECT-запросом без unique index
  • Не отключать retry полностью
  • Не подтверждать сообщение до надежной фиксации результата

Что подготовить для диагностики

  • Пример двух дублей
  • Message ID и operation ID
  • Код consumer и схема ack
  • Индексы таблицы заказов
  • Настройки retry и DLQ

Частые вопросы

Почему очередь доставляет сообщение дважды?

Это ожидаемо при at-least-once доставке: брокер не получил подтверждение или producer повторил публикацию.

Достаточно ли Redis-lock?

Нет. Lock снижает конкуренцию, но не заменяет уникальное ограничение и атомарную фиксацию.

Что вернуть при повторе?

Обычно существующий результат операции или успешное подтверждение без создания новой сущности.

Когда стоит обратиться за помощью

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

Итог

Дубли устраняются идемпотентностью на уровне бизнес-ключа и базы, а не запретом повторной доставки. Настроить consumer, outbox и миграцию можно через @rabotator_support.