Idempotency: nonce + enforce_nonce
Idempotency: nonce + enforce_nonce
Проблема идемпотентности и дедупликации
При ретраях (таймауты, мобильная сеть, двойной клик) клиент может отправить одно и то же сообщение несколько раз, и сервер создаст дубли.
Как в Discord
Discord поддерживает nonce (до 25 символов) и флаг enforce_nonce: если enforce_nonce=true и nonce присутствует, то nonce проверяется на уникальность “в пределах нескольких минут”; при повторе возвращается уже созданное сообщение и новое не создаётся.
Контракт
Клиент присылает:
nonce: str|int(<= 25 chars)enforce_nonce: bool
Поведение:
enforce_nonce=false->nonceне участвует в дедупликации.enforce_nonce=true+nonce-> дедупликация в коротком TTL-окне.
Реализация (идея)
Дедупликацию делаем не постоянной через уникальный индекс в Postgres, а “на 300 секунд” через Redis TTL.
Ключ: nonce:{author}:{nonce} -> значение message_id, TTL = через 300 секунд.
Алгоритм (концептуально):
При
enforce_nonce=trueвыполняем атомарныйSET key value NX EX ttl.Если
SETуспешен -> создаём сообщение в Postgres и сохраняемmessage_idв ключ (либо сразу пишемPENDING, затем обновляем наmessage_id).Если
SETнеуспешен -> читаемmessage_idиз Redis и возвращаем уже созданное сообщение.
Почему решение именно такое?
1) Семантика “на несколько минут”, а не навсегда
Мы сознательно не делаем дедупликацию через уникальный индекс в Postgres, потому что это даёт вечную идемпотентность: один и тот же ключ никогда больше не сможет создать новое сообщение.
В Discord же enforce_nonce означает проверку уникальности в пределах последних нескольких минут, и при совпадении возвращается уже созданное сообщение.
Подобные проблемы возникает только в рамках временных сбоев, поэтому решение в рамка 300 секунд валидно.
2) Атомарность и защита от гонок
Схема “at first SELECT/GET, then INSERT/SET” ломается при конкурентных запросах: два параллельных запроса могут одновременно увидеть отсуствие ключа и оба создать дубль.
Команда Redis SET key value NX EX ttl атомарна: ровно один запрос “заберёт” nonce-ключ, а остальные сразу поймут, что это повтор в окне TTL.
3) Производительность под нагрузкой
Проверка/установка ключа в Redis — это один быстрый сетевой round-trip и операция в памяти, поэтому dedup работает быстро даже при высоком QPS. Если бы мы полагались только на Postgres (unique + обработка конфликтов), мы бы чаще упирались в запись/индексы и повышали нагрузку на базу при массовых ретраях.
4) Контроль роста данных
TTL автоматически удаляет idempotency-ключи, поэтому они не накапливаются бесконечно и не требуют фоновой уборки. Это ровно то, что нужно для retry-окон: хранить “память о запросе” ограниченное время.
5) Единое поведение HTTP и WebSocket
Если HTTP и WS создают сообщения разными путями, WS может обходить дедупликацию и снова порождать дубли. Поэтому оба транспорта должны вызывать одну функцию “создать сообщение с учётом nonce/enforce”.
6) Почему Redis?
Redis даёт ограниченное по времени окно дедупликации, а не “вечную” идемпотентность.
Последнее обновление