Этот вопрос проверяет понимание идемпотентности и умение придумать устойчивый способ избежать повторной обработки событий, даже если у события нет явного ID.
Если внешний сервис не присылает явный идентификатор события, можно построить «искусственный» ключ идемпотентности на основе содержимого события. Например, взять хеш от важных полей (тип события, сумма, время, внешний user_id) и хранить его в таблице обработанных событий. При получении нового события вы вычисляете тот же хеш и проверяете, обрабатывали ли вы его раньше. Иногда дополнительно используют временное окно (например, хранить такие ключи сутки), чтобы не копить вечный список. Главное — аккуратно выбрать поля, чтобы одинаковые по смыслу события давали один и тот же ключ, а разные — разные.
Определение.
Идемпотентность — это свойство операции, при котором повторное выполнение имеет тот же эффект, что и первое, и не приводит к «дублированию» результата.
В контексте событий это означает:
Получив одно и то же событие несколько раз, мы:
Не создаём дубликаты записей.
Не списываем деньги повторно.
Не отправляем повторных уведомлений, если это критично.
Обычно для этого используется идентификатор события (event_id), но если его нет, нужно придумать его самим.
Есть несколько подходов, которые можно комбинировать.
Идея простая:
Выделить набор полей, которые однозначно определяют событие:
Тип события (event_type).
Пользователь (user_id или аналог).
Сумма/параметры (amount, currency).
Временная метка или «бизнесовый» ID (например, номер заказа).
Собрать из них строку/структуру и вычислить хеш (например, SHA-256).
Хранить этот хеш как «ключ идемпотентности» в БД.
Python
import hashlib
import json
def make_idempotency_key(event: dict) -> str:
key_data = {
"type": event.get("type"),
"user_id": event.get("user_id"),
"amount": event.get("amount"),
"order_id": event.get("order_id"),
}
payload = json.dumps(key_data, sort_keys=True)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
При обработке события:
Считаем ключ.
Смотрим в таблице processed_events:
Если ключ найден — считаем событие уже обработанным.
Если нет — обрабатываем и записываем ключ.
Иногда можно обойтись и без хеша, если есть естественный уникальный набор полей, например: (order_id, event_type).
Для каждой комбинации (order_id, event_type) делаем уникальный индекс в БД.
При попытке вставить второй раз БД бросит ошибку «уникальное ограничение нарушено».
В такой ситуации обработчик может интерпретировать это как «событие уже было».
Если идентификатор строится из «похожих» полей (например, сумма и время округлённое до минуты), есть риск случайных совпадений. Чтобы ограничить риск и размер таблицы:
Можно хранить ключи только какое-то время:
В Redis с TTL (например, 24 часа).
В БД с регулярной чисткой старых записей.
Типичный подход:
SETNX (или SET key value NX EX ttl в Redis) для вставки ключа.
Если ключ уже есть — считаем событие дубликатом.
Python (псевдокод для Redis)
def should_process_event(redis_client, key: str, ttl_seconds: int = 86400) -> bool:
# Вернёт True, если ключ успешно установлен (событие новое)
return redis_client.set(key, "1", ex=ttl_seconds, nx=True)
Важно понимать, что:
Если вы выберете слишком мало полей, разные события могут «сливаться» в один ключ — часть событий будет проигнорирована.
Если выберете слишком много полей (например, точное время с миллисекундами), одно и то же событие, отправленное дважды, может дать разные ключи — защита не сработает.
Практические советы:
Ориентируйтесь на «бизнесовый смысл»:
Для платежей — order_id + event_type.
Для подписок — subscription_id + period_start.
Для изменения баланса — account_id + «бизнесовый» ID операции.
Если внешнего «бизнесового» ID нет — попробуйте договориться с внешним сервисом, чтобы он его добавил. Иногда это проще и безопаснее.
Типичный сценарий:
Пришёл webhook без event_id.
Внутри обработчика:
Считаем idempotency_key.
Пытаемся записать его в БД/Redis.
Если запись прошла — продолжаем обработку (меняем баланс, создаём запись).
Если запись не прошла (ключ уже есть) — логируем и завершаем без повторных действий.
Важно:
Логировать такие случаи для дебага.
Следить за размером хранилища ключей.
Если нет явного ID, создаём искусственный ключ на основе содержимого события.
Храним ключи в БД или Redis и используем уникальность/TTL.
Аккуратно подбираем поля, чтобы минимизировать ложные совпадения и «пропуск» реальных дубликатов.
По возможности стараемся договориться с внешним сервисом о явном event_id или request_id — это всегда проще и надёжнее.
Краткий вывод:
При отсутствии ID события идемпотентность достигается за счёт искусственного «idempotency key» — хеша или комбинации бизнес-полей, которые однозначно описывают событие. Такой ключ сохраняется в устойчивом хранилище и позволяет безопасно игнорировать повторы без повторного выполнения критичных действий.