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

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

Именно в таком отношении ключевым образом отличаются, например, системы  Amazon SNS и Amazon EventBridge. SNS может предложить упорядоченную доставку через топики FIFO, но EventBridge — сервис шины событий AWS, рассчитанный на обслуживание слабосвязанных масштабируемых систем со множеством аккаунтов, такой доставки не обеспечивает. И это нормально. Подход  EventBridge по-прежнему наиболее подходит для большинства событийно-ориентированных систем, поскольку это не дело продьюсера, отвечать за упорядочение событий. Именно потребитель должен судить о состоянии событий, и не для каждого потребителя будет важен их порядок. Задача продьюсера — достаточно подробно описать в полезной нагрузке событий, в каких случаях требуется строго упорядочивать события, чтобы потребитель мог сам решать, нужно ли это.

В этом посте мы разберём:

  • ? Разницу между упорядоченной и неупорядоченной доставкой событий

  • ?️ Почему подход EventBridge — то, что надо для современных событийно-ориентированных систем

  • ? Как проектировать полезную нагрузку событий таким образом, чтобы потребители могли сами их упорядочивать

  • ? Как благодаря атомарному версионированию баз данных или использованию меток времени обеспечить безопасность и предсказуемость неупорядоченных событий

Дочитав статью, вы убедитесь, что «неупорядоченные» - это не «ненадёжные», а слабосвязанные, масштабируемые и умные ?.

? Проектирование умных событий

Умные события – самодостаточные, описательные и при этом версионируются. Они предоставляют потребителям тот контекст, по которому можно судить о состоянии и при этом не требовать от продьюсера гарантировать порядок доставки.

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

Вот пример хорошо структурированного события:

{
  "entityId": "user-123",
  "version": 5,
  "updatedAt": "2025-10-29T10:15:00Z",
  "eventType": "UserUpdated",
  "source": "user-service",
  "data": {
    "name": "Alice",
    "email": "alice@example.com"
  }
}

? Какие основные свойства следует включить

entityId обозначает предмет события.

version — это монотонно возрастающий номер, соответствующий версии объекта или агрегата.

updatedAt — это метка времени, соответствующая стандарту ISO 8601 и указывающая, когда именно произошло изменение.

eventType описывает природу изменения, например, UserCreated или UserUpdated.

source указывает, откуда поступило событие — эта информация полезна в окружениях, охватывающих много доменов.

data содержит конкретные детали об изменении состояния.

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

⚙️ Ответственность продьюсера

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

На практике выглядит так:

  • Увеличиваем значение version всякий раз, когда объект меняется.

  • В базе данных применяем атомарные изменения, чтобы не возникало конфликтов между версиями.

  • Всегда публикуем новейшую известную версию полезной нагрузки события.

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

?️ Управление версиями и временными метками в DynamoDB

В таких базах данных как Amazon DynamoDB просто организовать контроль версий и управление метками времени на уровне данных.

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

Так гарантируется, что, как только один процесс может успешно обновить запись. Благодаря этому не допускаются условия гонки и обеспечивается атомарность.   

Придерживаемся такого типичного паттерна:

  • В каждом элементе сохраняем атрибут version.

  • Используем выражение ConditionExpression, перед записью проверяющее version = :expectedVersion.

  • При успехе увеличиваем номер версии до следующего значения.

  • При конфликте предпринимаем повторную попытку или отбрасываем событие в зависимости от реализованной у вас логики событий.

Кроме того, DynamoDB поддерживает соответствующие стандарту ISO 8601 метки времени для полей, например, createdAt и updatedAt. Эти свойства помогают отслеживать, когда именно запись была создана и в последний раз обновлена. Также по ним можно определять свежесть событий или порядок их следования во времени. В соответствии со стандартом ISO 8601 строки сортируются строго в словарном порядке, благодаря чему их легко сравнивать и запрашивать.

Сочетая атомарный атрибут version с метками времени createdAt и updatedAt, можно естественным образом поддерживать в вашей модели данных неупорядоченную доставку событий, в то же время обеспечивая согласованность на уровне продьюсеров и потребителей.

? Порядок, зависящий от потребителей

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

Цель потребителя проста: хранить или обрабатывать наиболее свежую версию данных.

? Обработка версионированных событий

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

Вот простой пример такой логики:

if (incoming.version > stored.version) {
  updateRecord(incoming)
}

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

?️ Гарантирование атомарных обновлений

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

Например, работая с DynamoDB, можно при помощи ConditionExpression обновлять запись лишь в том случае, когда сохранённая версия ниже, чем поступившая:

await dynamoDb.update({
  TableName: "Users",
  Key: { entityId: incoming.entityId },
  UpdateExpression: "SET #data = :data, #version = :version",
  ConditionExpression: "attribute_not_exists(#version) OR #version < :version",
  ExpressionAttributeNames: {
    "#data": "data",
    "#version": "version"
  },
  ExpressionAttributeValues: {
    ":data": incoming.data,
    ":version": incoming.version
  }
})

Если условие не выполняется, это означает, что запись уже существует в более новой версии, поэтому обновление безопасно пропускается.

? Использование updatedAt для упорядочивания и согласования

Ещё один надёжный способ управления свежестью событий — при помощи свойства updatedAt.

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

При  использовании DynamoDB это можно реализовать при помощи условных обновлений. Такое обновление проходит успешно лишь в том случае, когда значение  updatedAt у входящего события выше, чем уже сохранённое. Временные метки, соответствующие стандарту ISO 8601, корректно сравниваются как строки, благодаря чему этот паттерн получается простым и безопасным в использовании.

Вот пример на TypeScript с использованием AWS SDK v3:

await ddb.send(new UpdateCommand({
    TableName: "Users",
    Key: { entityId: incoming.entityId },
    UpdateExpression: "SET #data = :data, #updatedAt = :updatedAt",
    ConditionExpression: "attribute_not_exists(#updatedAt) OR :updatedAt > #updatedAt",
    ExpressionAttributeNames: {
      "#data": "data",
      "#updatedAt": "updatedAt",
    },
    ExpressionAttributeValues: {
      ":data": incoming.data,
      ":updatedAt": incoming.updatedAt,
    },
  }));

Если условие не выполняется, то DynamoDB выбрасывает исключение ConditionalCheckFailedException — это означает, что сохранённая запись новее, чем поступившая. Потребитель может без опаски отловить это исключение и пропустить обновление.

Так гарантируется согласованность данных, даже если неупорядоченно поступает множество событий, касающихся одного и того же объекта.

? Разные потребители, разные потребности

Не всем потребителям важен порядок событий.

  • В поисковом индексе часто требуется только самое последнее состояние.

  • Службе аудита потребуется сохранять все события без исключения, независимо от их порядка.

  • Служба уведомлений может интересоваться лишь событиями определённых типов.

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

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

?️ Почему EventBridge выигрывает

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

? Сравнение Amazon SNS and Amazon EventBridge

⚙️ Понимание компромиссов

В SNS FIFO обеспечивается строгое упорядочивание событий внутри группы, но каждая группа сериализуется, что существенно ограничивает пропускную способность. По мере того, как объём событий растёт, возникает узкое место при масштабировании. Кроме того, для работы топиков FIFO требуются публикаторы, которые будут определять ID группы сообщений и последовательно его поддерживать. Из-за этого сложность только возрастает.

Стандарт SNS обеспечивает очень высокую пропускную способность, но не гарантирует порядок следования или строго однократную доставку. Этот метод прост и быстр, но не подходит для систем, полагающихся при работе на упорядоченную доставку или сложную логику маршрутизации..

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

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

? Вывод

Если работа вашей архитектуры зависит от порядка следования событий, то в таких узких прикладных случаях можно воспользоваться SNS FIFO или SQS FIFO. Но в большинстве современных распределённых приложений требуется более высокий уровень абстрагирования и гибкости, такой, как у EventBridge.

Проектируя события с такими свойствами как version, updatedAt и entityId, вы избавляетесь от необходимости гарантировать строгий порядок доставки, а пользуясь EventBridge значительно выигрываете в масштабировании, маршрутизации и интеграции с экосистемой.

⏱️ В каких случаях порядок следования всё равно важен

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

Хороший пример – финансовые транзакции. Если две операции будут обработан в неверном порядке, то на выходе могут оказаться некорректными баланс или статус счёта. То же касается логов аудита, систем управления потоками задач или паттернов event sourcing — они зачастую зависят от точной последовательности событий, так как именно по ней можно воспроизвести или реконструировать историю.

В таких ситуациях может быть приемлемо использовать сервис, гарантирующий упорядоченную доставку. Строгое упорядочение сообщений внутри определённой группы обеспечивается в FIFO-топиках SNS или FIFO-очередях SQS; пользуясь такими инструментами, можно гарантировать, что все сообщения будут обрабатываться в верной последовательности. Но при работе с этими решениями приходится идти на определённые компромиссы, например:

  • Снижать пропускную способность, так как сообщения внутри группы сериализуются

  • Усиливать связность между продьюсерами и потребителями

  • Усложнять рабочие нагрузки, связанные с масштабированием и сегментированием

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

Даже в таких случаях всё равно ценно включать в ваши события такие метаданные как version и updatedAt. Эти свойства служат дополнительной страховкой и позволяют потребителям проверять согласованность данных в случае воспроизведения, повторных попыток или двойной доставки.

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

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

? Заключение: публикуйте умные события

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

Команда, стремящаяся добиться успеха при публикации событий, должна:

  • Включать в каждое событие ясные и непротиворечивые метаданные, такие как version, updatedAt и entityId.

  • Следовать надёжным паттернам, представляя в виде событий переходы от состояния к состоянию.

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

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

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

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


P. S. Обращаем ваше внимание на то, что у нас на сайте проходит распродажа.

Комментарии (0)