Короткий ответ
Идемпотентность достигается явным uniqueness-ограничением на идентификатор операции (idempotency key/event id) в таблице, где фиксируется факт обработки, в сочетании с атомарной записью результата и факта обработки в одной транзакции — повторная обработка того же события натыкается на уже существующую запись и безопасно пропускается вместо повторного выполнения эффекта.
Как это работает подробнее
Перед выполнением бизнес-эффекта приложение пытается вставить строку с уникальным event_id в таблицу processed_events в той же транзакции, где выполняется сам эффект; если вставка нарушает unique-ограничение — событие уже обработано, транзакция откатывается без повторного эффекта.
Сравнение
- Компромисс. Хранение полной истории обработанных event_id бессрочно требует места и обслуживания (архивация/партиционирование), а слишком агрессивная очистка рискует «забыть» о событии при очень отложенном ретрае брокера; критерий выбора срока хранения — максимально возможная задержка повторной доставки в конкретном брокере против стоимости хранения большого объёма идентификаторов.
Пример кода
example.py
from django.db import transaction, IntegrityError
def handle_payment_event(event_id: str, account_id: int, amount: int) -> bool:
try:
with transaction.atomic():
ProcessedEvent.objects.create(event_id=event_id) # unique constraint на event_id
Account.objects.filter(id=account_id).update(balance=F("balance") - amount)
return True # можно делать ack сообщения брокеру
except IntegrityError:
# event_id уже обработан ранее — эффект не повторяем, но ack всё равно делаем
return FalseЧто использовать на практике
- Практика. Ключ идемпотентности должен однозначно идентифицировать именно бизнес-операцию, а не технический id сообщения в очереди (который может отличаться при ретрае брокера); для внешнего side effect (вызов платёжного API) идемпотентность нужно обеспечивать и на уровне самого внешнего вызова.
Подводные камни
- Ошибка. Проверяют «не обработано ли уже событие» отдельным SELECT, а запись выполняют отдельной операцией позже — между шагами возможна гонка при параллельной обработке; используют как ключ идемпотентности значение, меняющееся при каждой повторной доставке.