АрхитектураАрхитектура ПОАрхитектор распределённых систем

Событийный обработчик может получить одно и то же событие повторно из за повтора доставки. Какое свойство о...

Событийный обработчик может получить одно и то же событие повторно из-за повтора доставки. Какое свойство операции не позволяет повторно изменить бизнес-состояние?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Такое свойство называется идемпотентностью: повторное применение одного и того же события приводит к тому же итоговому состоянию, что и его однократная обработка. Обычно этого достигают проверкой уникального идентификатора события или естественным идемпотентным обновлением состояния.

Идемпотентность не означает, что обработчик выполняется только один раз. Она делает повторную доставку безопасной.

Исторический контекст

Асинхронные сообщения и события применяют, чтобы уменьшить синхронную связанность компонентов, изолировать временные сбои и позволить обработчикам работать независимо. При этом доставка сообщения часто строится по модели at-least-once: система старается доставить сообщение, но при сбое подтверждения может отправить его повторно.

Такой компромисс обычно надёжнее, чем попытка гарантировать глобальную доставку ровно один раз. Поэтому ответственность за безопасную повторную обработку переносится на потребителя события.

Постановка проблемы

Предположим, событие сообщает о списании средств. Обработчик уменьшил баланс, но завершился до отправки подтверждения брокеру. Брокер считает доставку не подтверждённой и отправляет событие ещё раз.

Если обработчик просто повторит операцию, деньги или другое бизнес-состояние будут изменены дважды. Повтор может возникнуть также из-за сетевого сбоя, тайм-аута, повторной публикации или восстановления потребителя после сбоя.

Подробное решение

Обработчик должен распознавать повтор и не применять бизнес-эффект второй раз. Для этого событие должно иметь стабильный уникальный идентификатор, а потребитель — хранить информацию о уже обработанных событиях в надёжном хранилище.

Типичный механизм выглядит так:

  1. Потребитель получает идентификатор события.
  2. В одной транзакции проверяет, не было ли событие обработано ранее.
  3. Если событие новое, применяет изменение бизнес-состояния и фиксирует его идентификатор.
  4. Если идентификатор уже зафиксирован, повторно выполняет безопасный путь без изменения состояния.

Критично, чтобы отметка об обработке и изменение бизнес-состояния были согласованы. Если сначала записать отметку, а затем упасть до изменения состояния, событие будет ошибочно считаться обработанным. Если сначала изменить состояние, а затем не записать отметку, повтор может снова применить операцию.

Идемпотентность можно получить и без отдельного журнала событий. Например, операция может устанавливать состояние в конкретное значение, а не увеличивать счётчик на единицу. Однако это возможно только если бизнес-смысл операции допускает такую форму и повтор действительно не меняет результат.

Отдельное хранилище обработанных сообщений защищает от повторов, но не решает все проблемы. Нужно определить срок хранения идентификаторов, поведение при повторе события с тем же идентификатором, но другим содержимым, а также обработку событий, пришедших в другом порядке.

Идемпотентность не устраняет дубликаты и не гарантирует правильный порядок событий. Она также не заменяет атомарность, контроль версий, дедупликацию на границе приёма и компенсационные операции, когда действие нельзя безопасно повторить или отменить.

Ситуация из практики

Сервис заказов публиковал событие о подтверждённом заказе, а сервис резервирования товаров создавал резерв. При временном сбое подтверждение публикации повторялось, и один заказ резервировал товар дважды.

Рассматривались три варианта. Гарантия доставки ровно один раз выглядела простой для потребителя, но не устраняла повторы на всех границах системы и усложняла инфраструктуру. Дедупликация только в брокере не защищала от повторной обработки после передачи сообщения потребителю. Компенсационные операции позволяли исправлять ошибку, но временно создавали неверное состояние и требовали дополнительной бизнес-логики.

Выбрали идемпотентный обработчик с уникальным идентификатором события и уникальным ограничением на пару идентификатора заказа и типа резерва. Запись о резерве и факт обработки события сохранялись согласованно. Повторная доставка стала безопасной: первый вызов создавал резерв, последующие обнаруживали уже выполненную операцию и не меняли остаток.

Что кандидаты часто упускают

1. Достаточно ли проверить, что событие уже обработано, перед выполнением операции?

Нет, если проверка и изменение бизнес-состояния не защищены общей транзакцией или эквивалентным механизмом атомарности. Между проверкой и записью может произойти сбой, а два параллельных экземпляра обработчика могут одновременно решить, что событие новое.

Нужно атомарно связать дедупликацию с бизнес-изменением либо использовать конфликт уникальности как механизм сериализации. Если состояние находится в другой системе, одной локальной транзакции недостаточно и потребуются идемпотентный внешний вызов, журналирование результата или компенсация.

2. Чем идемпотентность отличается от обработки события ровно один раз?

Обработка ровно один раз утверждает, что бизнес-операция будет выполнена только однажды. Идемпотентность допускает многократные попытки, но гарантирует одинаковый итоговый бизнес-результат.

Для распределённой системы второй подход обычно практичнее: повторы доставки и выполнения возможны, поэтому безопасность обеспечивается свойствами операции и согласованной фиксацией результата, а не предположением об отсутствии повторов.

3. Спасает ли идемпотентность от событий, пришедших в неправильном порядке?

Нет. Она защищает от повторного применения одного события, но не определяет, можно ли применять более старое событие после нового.

Для управления порядком нужны дополнительные механизмы: версия сущности, номер последовательности, проверка допустимого перехода состояния или хранение событий до получения пропущенных. Если порядок не гарантируется и события конфликтуют, система должна явно определить политику: отклонять устаревшие сообщения, применять их слияние или запускать корректирующее событие.