Как избежать дублирования выполнения задач и спама пользователя при повторной обработке сообщений из очереди
Что проверяют
Понимаете ли вы, что брокер почти всегда доставляет сообщения по схеме «хотя бы один раз», и дубли это нормальный режим работы, а не авария. Следом обычно спрашивают: что будет, если сервис упал после отправки письма, но до фиксации offset.
Как отвечать
Начните с причины: Kafka и RabbitMQ повторяют сообщение, если consumer не успел его подтвердить, упал или попал под ребалансировку. Exactly-once в Kafka покрывает только цепочку «прочитал из Kafka, записал в Kafka» через транзакции, на отправку SMS или письма он не распространяется. Поэтому защита строится в обработчике:
- ключ идемпотентности: стабильный идентификатор, который присваивает продюсер, либо бизнес-ключ вроде
orderId + eventType. Генерировать его на стороне consumer нельзя; - таблица обработанных сообщений:
processed_messagesс уникальным ограничением по ключу. Вставка черезINSERT ... ON CONFLICT DO NOTHINGв той же транзакции, что и бизнес-изменение: не вставилось, значит дубль, выходим; - статус побочного эффекта: у уведомления есть состояние
NEW,SENT,FAILED, отправка идёт только изNEW. Если провайдер рассылки принимает ключ идемпотентности, передавайте и его; - outbox: событие «отправить письмо» пишется в таблицу вместе с бизнес-данными, а отдельный процесс доставляет его наружу;
- ограничение повторов: в Spring Kafka это
DefaultErrorHandlerс backoff иDeadLetterPublishingRecoverer, в RabbitMQ dead letter exchange. Отравленное сообщение уходит в DLQ, а не крутится по кругу; - защита от спама: лимит уведомлений на пользователя, например
SET key NX EX 3600в Redis. Это страховка на случай ошибки в логике.
Ключи дедупликации храните дольше, чем максимальное окно повторной доставки.
Где спотыкаются
Предлагают HashSet в памяти. Он теряется при рестарте и не работает, когда запущено несколько экземпляров сервиса.
Делают «проверил, потом вставил» двумя запросами без уникального индекса. Два параллельных обработчика проходят проверку одновременно, и письмо уходит дважды.
Отмечают сообщение обработанным до побочного эффекта или коммитят offset до обработки. Дублей нет, зато при падении сообщение теряется молча.