Как избежать дублирования уведомлений при отправке пушей через Kafka
Что проверяют
Понимаете ли вы, что Kafka по умолчанию доставляет сообщения «хотя бы один раз», и дубль на стороне потребителя это норма, а не авария. Следом почти всегда спрашивают: почему бы не включить exactly-once и не забыть о проблеме?
Как отвечать
Начните с источников дублей:
- ретраи продюсера: сообщение записалось, а подтверждение потерялось. Лечится
enable.idempotence=true. В Java-клиенте это значение по умолчанию, а у librdkafka, на которой построенconfluent-kafka-go, оно выключено, так что в Go проверяйте настройку явно; - повторная доставка потребителю: сервис отправил пуш и упал до коммита оффсета, или группа ушла в перебалансировку. После рестарта сообщение придёт снова;
- дубли на входе: бизнес-событие опубликовали дважды, оффсеты разные, смысл один.
Главный инструмент это ключ идемпотентности. Продюсер кладёт в сообщение notification_id, собранный из бизнес-данных (пользователь, событие, тип), а не случайный UUID на каждую попытку. Потребитель перед отправкой атомарно занимает ключ: SET notif:<id> 1 NX EX 86400 в Redis или INSERT ... ON CONFLICT DO NOTHING в PostgreSQL с уникальным индексом. Не удалось занять, значит сообщение уже обработано: коммитим оффсет и идём дальше.
Честно назовите компромисс. Пометили до отправки и упали, пуш потерян; пометили после, возможен дубль. Обычно хранят статус pending/sent и повторяют только зависшие pending. Последняя страховка на стороне провайдера: apns-collapse-id у APNs и tag в Android-уведомлении FCM. Повторный пуш с тем же значением заменит предыдущий в шторке, а не встанет рядом.
Где спотыкаются
Говорят «включим транзакции Kafka, и будет exactly-once». Транзакции дают атомарность только внутри Kafka: прочитал, обработал, записал в другой топик. Вызов APNs или FCM это внешний побочный эффект, откатить его транзакция не может.
Проверяют ключ через GET, а потом делают SET. Между двумя командами второй экземпляр успевает сделать то же самое, нужна одна атомарная операция.
Генерируют идентификатор в потребителе или заново при каждом ретрае продюсера. Тогда у повторного сообщения новый ключ, и дедупликация ничего не ловит.