События изменений одного заказа иногда приходят потребителю не по порядку. Какой механизм контракта позволяет безопасно обработать такую доставку?
События одного агрегата должны содержать монотонную версию состояния, например номер изменения. Потребитель хранит последнюю применённую версию: устаревшее событие игнорирует, а при обнаружении пропуска версии временно откладывает обработку и запрашивает недостающие изменения или актуальный снимок.
Одной надежды на порядок доставки недостаточно: порядок может нарушиться из-за повторной доставки, параллельной обработки, восстановления после сбоя или особенностей маршрутизации сообщений.
В монолитной транзакции изменения обычно читаются в рамках одного хранилища, где порядок фиксации и видимость данных контролируются общей системой. После разделения приложения на сервисы изменения передаются через сеть, а обработка и доставка становятся независимыми.
Событийная интеграция появилась, в частности, чтобы уменьшить связанность между сервисами и позволить им работать независимо. Но такая независимость означает, что потребитель должен сам проверять, применяет ли он актуальное изменение, а не полагаться на глобальный порядок сообщений.
Пусть заказ последовательно перешёл из состояния «создан» в «оплачен», а затем в «отправлен». Если событие об отправке будет применено раньше события об оплате, потребитель может построить невозможное состояние, выполнить неверное действие или отправить неправильное уведомление.
Проблема усугубляется тем, что позднее событие не всегда можно отличить от повторной доставки без дополнительного признака. Простое сравнение времени создания ненадёжно: часы разных узлов могут расходиться, а время публикации не обязательно отражает порядок бизнес-изменений.
Владелец агрегата присваивает каждому изменению последовательную версию: например, 1, 2, 3. Версия должна отражать порядок изменений конкретного заказа и фиксироваться согласованно с самим изменением состояния. Событие содержит идентификатор агрегата и его версию.
Потребитель хранит последнюю успешно применённую версию для каждого заказа:
При пропуске потребитель может повторить чтение потока, запросить недостающие события у источника или получить полный снимок состояния. После восстановления последовательности отложенное событие обрабатывается заново. Конкретный вариант зависит от того, поддерживает ли инфраструктура повторное чтение и может ли источник предоставить историю изменений.
Версия обычно должна быть отдельной частью контракта, а не вычисляться потребителем. Она относится к конкретному агрегату, поэтому глобальный счётчик для всех заказов обычно создаёт лишнюю связанность и ограничивает масштабирование.
Механизм не решает все задачи согласованности. Если бизнес-правило требует увидеть изменения нескольких агрегатов в одной атомарной операции, версии событий не заменяют распределённую координацию или пересмотр границ сервиса. Кроме того, порядок событий одного агрегата не означает порядок событий разных агрегатов.
Компромисс заключается в задержке: обнаружив пропуск, потребитель может временно остановить обработку событий этого заказа. Если же продолжить обработку сразу, система получит более высокую доступность ценой риска некорректного состояния. Для независимых или нестрогих обновлений допустима политика «последнее известное состояние», но её нужно явно закрепить контрактом.
Сервис заказов публикует события, а сервис доставки строит по ним маршрут. После восстановления одного из узлов событие с версией 12 поступило раньше события с версией 11. Без проверки версии доставка увидела заказ как уже отправленный, а затем получила более раннее состояние и ошибочно вернула его в статус «оплачен».
Рассматривались три варианта. Можно было полностью полагаться на порядок брокера, но это не защищало от повторной публикации, повторного чтения и параллельных потребителей. Можно было всегда запрашивать полный заказ, что упрощало восстановление, но увеличивало нагрузку и не давало понять, пропущено ли промежуточное бизнес-изменение. Третий вариант — добавить версию агрегата и обрабатывать пробелы отдельно.
Выбрали третий вариант: событие с версией 12 откладывалось, сервис доставки запрашивал недостающий диапазон, а при недоступности истории получал снимок заказа. Результатом стала защита от регрессии состояния без требования глобального порядка и без постоянного чтения полной сущности.
Нет, временная метка не гарантирует порядок бизнес-изменений. На него влияют рассинхронизация часов, различия между временем создания, фиксации и публикации, а также одинаковые или недостаточно точные значения времени. Временная метка может быть полезна для диагностики, но надёжный контроль последовательности должен опираться на версию, назначенную владельцем состояния.
Только если контракт явно гарантирует, что каждое событие содержит полное актуальное состояние и промежуточные изменения не имеют самостоятельного значения. Для событий, описывающих дельту или важный переход, это небезопасно: потребитель может пропустить обязательное действие или неверно вычислить состояние. Поэтому при обнаружении разрыва нужно либо восстановить пропущенные события, либо получить согласованный снимок.
Она должна сохраняться как часть состояния обработки потребителя и обновляться атомарно с результатом применения события. Если обновить версию отдельно, сбой между двумя операциями может привести к потере события или его повторной обработке. Повторная обработка всё равно должна быть безопасной, но атомарная фиксация версии и результата уменьшает окно несогласованности и делает прогресс потребителя наблюдаемым.