Паттерн Идемпотентный Потребитель. Окончание
Начало
Продолжение
Детерминированные и недетерминированные обработчики
Что произойдёт, когда ваш обработчик вызывает что-то вне базы данных: API, отправка email, платёжный шлюз или очередь фоновых заданий? Всё это распространённые побочные эффекты, которые также должны быть идемпотентными.
Эти вызовы находятся за пределами транзакции. БД может успешно завершить транзакцию, но, если сеть перестанет работать до ответа внешнего сервиса, вы не сможете определить, произошло действие или нет. При повторной попытке клиент может отправить ещё один email или дважды списать средства с карты.
Мы вступили на сложную территорию недетерминированных обработчиков: операции, которые невозможно безопасно повторить. Есть две основные стратегии решения этой проблемы.
1. Использовать ключ идемпотентности во внешнем вызове
Если внешний сервис это поддерживает, передавайте стабильный идентификатор, например, MessageId сообщения, с каждым запросом. Многие API, включая платёжные системы и платформы email, позволяют указывать заголовок c ключом идемпотентности. Сервис гарантирует, что идентичные запросы с одним и тем же ключом будут выполнены только один раз:
await emailSvc.SendAsync(new EmailRequest
{
To = user.Email,
Subject = "Привет!",
Body = "Спасибо за подписку на канал.",
IdempotencyKey = ctx.MessageId
});
Даже если запрос будет повторён, провайдер распознает ключ и отбросит дубликат. Это самый простой и надёжный подход, если внешняя зависимость его поддерживает.
2. Сохраните намерение локально
Если внешний сервис не поддерживает ключи идемпотентности, вы можете сымитировать его. Сохраните запись о предполагаемом действии в базе перед вызовом внешней системы. Например, таблица PendingEmails, содержащая информацию о сообщениях на отправку.
Фоновый процесс может позже читать эти записи и выполнять действие один раз. Это делает процесс детерминированным, но ценой большей сложности, дополнительных таблиц и фоновых процессов. Это часто оверинжиниринг, если только побочный эффект не является критическим или необратимым, например, платёж.
Компромисс сводится к оценке последствий. Если последствия повторения действия существенны, явно добавьте идемпотентность. В противном случае повтор операции может быть приемлемым.
Когда идемпотентный потребитель не нужен
Если операция изначально идемпотентна, часто можно пропустить дополнительную табличную и транзакционную логику.
Установка флага состояния или обновление кэша — примеры детерминированных действий, которые можно безопасно выполнять несколько раз. Это операции, которые перезаписывают состояние, а не добавляют к нему данные.
Некоторые обработчики также используют проверки предусловий, чтобы избежать дублирования. Если обработчик обновляет сущность, он может сначала проверить, находится ли эта сущность уже в желаемом состоянии, и вернуть управление раньше времени. Этого простого защитного условия может быть достаточно.
Не применяйте шаблон «Идемпотентный потребитель» бездумно везде. Применяйте его там, где он защищает вас от реального ущерба, где дублирование обработки приводит к финансовым последствиям или несогласованности данных.
Во всём остальном — чем проще, тем лучше.
Источник: https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it