Паттерн Идемпотентный Потребитель. Начало
Распределенные системы по своей природе ненадёжны. Одна из ключевых проблем — гарантировать, что сообщения обрабатываются ровно один раз. Теоретически это невозможно гарантировать в большинстве систем. Если вы проектируете систему, предполагая, что каждое сообщение будет обработано ровно один раз, вы подвергаете себя риску неявного повреждения данных. Но мы можем спроектировать систему так, чтобы побочные эффекты применялись ровно один раз, используя паттерн «Идемпотентный потребитель».
Что может пойти не так при публикации
Предположим, сервис публикует событие при создании новой заметки:
await publisher.PublishAsync(
new NoteCreated(note.Id, note.Title, note.Content));
Конкретная реализация издателя или брокера сообщений не важна.
Теперь представьте:
- Издатель отправляет сообщение брокеру.
- Брокер сохраняет его и отправляет ACK.
- Сбой в сети: ACK не доходит до производителя.
- Производитель по тайм-ауту повторяет попытку публикации.
- Теперь у брокера два события NoteCreated.
С точки зрения производителя, он действовал правильно. Но потребитель получил два события о создании одной и той же заметки.
И это только один из путей возникновения сбоя. Вы также можете получить дубликаты из-за:
- Повторных доставок брокером.
- Сбоев потребителя + повторных попыток.
Т.е. даже если вы всё сделали «правильно» на издателе, потребителю всё равно придётся защищаться.
Идемпотентность на стороне издателя (управляет брокер)
Многие брокеры сообщений поддерживают идемпотентную публикацию посредством дедупликации сообщений, если вы укажете уникальный идентификатор сообщения. Например, Azure Service Bus может обнаруживать дубликаты и игнорировать повторные публикации сообщений с тем же идентификатором в течение заданного периода. Amazon SQS и другие брокеры также предлагают аналогичные гарантии.
Не нужно заново изобретать эту логику в вашем приложении. Ключ к успеху — назначить каждому сообщению стабильный идентификатор, уникально отражающий отправляемое логическое событие. Например, при публикации события NoteCreated:
var message = new NoteCreated(
note.Id, note.Title, note.Content)
{
MessageId = Guid.NewGuid() // либо note.Id
};
await publisher.PublishAsync(message);
Если происходит сбой сети после отправки сообщения, приложение может повторить попытку. Но когда брокер обнаруживает тот же MessageId, он понимает, что это дубликат, и безопасно отбрасывает его. Вы получаете дедупликацию без каких-либо специальных таблиц отслеживания или дополнительного состояния в вашем сервисе.
Эта идемпотентность на уровне брокера решает широкий класс проблем на стороне производителя: повторные попытки сети, временные сбои и дублирующиеся публикации.
Она не обрабатывает повторные попытки потребителя, которые происходят при повторной доставке сообщений или сбое вашего сервиса во время обработки.
С этим поможет шаблон «Идемпотентный потребитель».
Продолжение следует…
Источник: https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it