Очереди обычно гарантируют доставку как минимум один раз, поэтому повтор сообщения — нормальная ситуация. Если 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.