Использование СУБД в качестве хранилища источников событий

Если бы я использовал RDBMS (например, SQL Server) для хранения данных источника данных, как могла бы выглядеть схема?

Я видел несколько вариаций, которые обсуждались в абстрактном смысле, но ничего конкретного.

Например, предположим, что у вас есть объект "Продукт", и изменения в этом продукте могут быть представлены в виде: Цена, Стоимость и Описание. Я смущен тем, что:

  • У вас есть таблица "ProductEvent", в которой есть все поля для продукта, где каждое изменение означает новую запись в этой таблице, а также "кто, что, где, почему, когда и как" по мере необходимости. Когда изменяется стоимость, цена или описание, добавляется вся новая строка для представления Продукта.
  • Сохранять стоимость продукта, цену и описание в отдельных таблицах, соединенных с таблицей продуктов с отношением внешнего ключа. Когда происходят изменения этих свойств, напишите новые строки с WWWWWH, если это необходимо.
  • Храните WWWWWH, а также сериализованный объект, представляющий событие, в таблице "ProductEvent", то есть само событие должно быть загружено, де-сериализовано и повторно воспроизведено в моем коде приложения, чтобы перестроить состояние приложения для данный продукт.

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

Я уверен, что "это зависит", и пока нет единого "правильного ответа", я пытаюсь понять, что приемлемо, и что совершенно неприемлемо. Я также знаю, что NoSQL может помочь здесь, где события могут быть сохранены против совокупного корня, что означает только один запрос к базе данных, чтобы получить события, чтобы перестроить объект, но мы не используем базу данных NoSQL на момент, поэтому я чувствую вокруг альтернатив.

Ответ 1

Хранилище событий не должно знать о конкретных полях или свойствах событий. В противном случае каждая модификация вашей модели приведет к необходимости переноса вашей базы данных (как и в случае старомодного состояния на основе состояния). Поэтому я бы не рекомендовал варианты 1 и 2 вообще.

Ниже приведена схема, используемая в Ncqrs. Как вы можете видеть, таблица "События" хранит связанные данные в виде CLOB (то есть JSON или XML). Это соответствует вашему варианту 3 (только, что нет таблицы "ProductEvents", потому что вам нужна только одна общая таблица "События" . В Ncqrs сопоставление с вашими сводными корнями происходит через таблицу "EventSources", где каждый EventSource соответствует фактическому Агрегатный корень.)

Table Events:
    Id [uniqueidentifier] NOT NULL,
    TimeStamp [datetime] NOT NULL,

    Name [varchar](max) NOT NULL,
    Version [varchar](max) NOT NULL,

    EventSourceId [uniqueidentifier] NOT NULL,
    Sequence [bigint], 

    Data [nvarchar](max) NOT NULL

Table EventSources:
    Id [uniqueidentifier] NOT NULL, 
    Type [nvarchar](255) NOT NULL, 
    Version [int] NOT NULL

Механизм сохранения SQL Реализация хранилища событий Джонатана Оливера состоит в основном из одной таблицы под названием "Commits" с полем BLOB "Полезная нагрузка". Это почти то же самое, что и в Ncqrs, только то, что он сериализует свойства события в двоичном формате (который, например, добавляет поддержку шифрования).

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

Схема его прототипической таблицы "События" гласит:

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]

Ответ 2

Проект GitHub CQRS.NET имеет несколько конкретных примеров того, как вы можете создавать EventStores в нескольких различных технологиях. На момент написания этой статьи в SQL реализована реализация с использованием Linq2SQL и схемы SQL, одна для MongoDB, одна для DocumentDB (CosmosDB, если вы в Azure) и одна, использующая EventStore (как упоминалось выше). В Azure есть еще кое-что, например Table Storage и Blob storage, которое очень похоже на плоское хранилище файлов.

Я предполагаю, что суть в том, что все они соответствуют одному принципу/контракту. Все они хранят информацию в одном месте/контейнере/таблице, они используют метаданные для идентификации одного события из другого и "просто" сохраняют все событие таким, каким оно было - в некоторых случаях сериализованным, как и поддерживающие технологии. Таким образом, в зависимости от того, выберете ли вы базу данных документов, реляционную базу данных или даже простой файл, есть несколько разных способов достижения одного и того же намерения хранилища событий (это полезно, если вы передумаете в любой момент и обнаружите, что вам нужно перенести или поддержать более одной технологии хранения).

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

Во-первых, мы обнаружили (даже с уникальными UUID/GUID вместо целых чисел) по многим причинам последовательные идентификаторы встречаются по стратегическим причинам, поэтому просто наличие идентификатора не было достаточно уникальным для ключа, поэтому мы объединили наш столбец ключа основного идентификатора с данными/Тип объекта для создания того, что должно быть действительно (в смысле вашего приложения) уникальным ключом. Я знаю, что некоторые люди говорят, что вам не нужно хранить его, но это будет зависеть от того, находитесь ли вы на новом месте или вам необходимо сосуществовать с существующими системами.

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

Еще одна вещь, которую следует помнить, это то, что требование действия X может привести к множеству различных событий, таким образом, зная все события, сгенерированные командой/событием/тем, что когда-либо полезно. Они также могут относиться к различным типам объектов, например, нажатие кнопки "купить" в корзине может вызвать срабатывание событий учетной записи и складирования. Приложение-потребитель может хотеть знать все это, поэтому мы добавили CorrelationId. Это означало, что потребитель мог запросить все события, возникшие в результате их запроса. Вы увидите это в схеме.

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

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

Ответ 3

Ну, вы можете взглянуть на Datomic.

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

Я написал подробный ответ здесь

Вы можете посмотреть разговор от Стюарта Хэллоуя, объясняющего дизайн Datomic здесь

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

Ответ 4

Возможный намек - это дизайн, за которым следует "Медленное изменение размера" (тип = 2), чтобы помочь вам:

  • порядок событий (через суррогатный ключ)
  • долговечность каждого состояния (действительная от - действительна до)

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

Ответ 5

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

Кроме того, помните, что хранилище событий должно использоваться только для записей, вы запрашиваете его только для загрузки событий, а не для свойств агрегата. Это отдельные вещи (это суть CQRS).

Решение 3 Что обычно делают люди, есть много способов сделать это.

Например, EventFlow CQRS при использовании с SQL Server создает таблицу со следующей схемой:

CREATE TABLE [dbo].[EventFlow](
    [GlobalSequenceNumber] [bigint] IDENTITY(1,1) NOT NULL,
    [BatchId] [uniqueidentifier] NOT NULL,
    [AggregateId] [nvarchar](255) NOT NULL,
    [AggregateName] [nvarchar](255) NOT NULL,
    [Data] [nvarchar](max) NOT NULL,
    [Metadata] [nvarchar](max) NOT NULL,
    [AggregateSequenceNumber] [int] NOT NULL,
 CONSTRAINT [PK_EventFlow] PRIMARY KEY CLUSTERED 
(
    [GlobalSequenceNumber] ASC
)

где:

  • GlobalSequenceNumber: простая глобальная идентификация, может использоваться для упорядочения или идентификации пропущенных событий при создании вашей проекции (readmodel).
  • BatchId: идентификация группы событий, которые были вставлены атомарно (TBH, понятия не имею, почему это было бы полезно)
  • AggregateId: идентификация агрегата
  • Данные: Сериализованное событие
  • Метаданные: другая полезная информация о событии (например, тип события, используемый для десериализации, метка времени, идентификатор отправителя из команды и т.д.)
  • AggregateSequenceNumber: порядковый номер в том же агрегате (это полезно, если вы не можете иметь записи, происходящие не по порядку, поэтому вы используете это поле для оптимистичного параллелизма)

Однако, если вы создаете с нуля, я бы рекомендовал следовать принципу YAGNI и создавать с минимально необходимыми полями для вашего варианта использования.

Ответ 6

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

https://github.com/andrewkkchan/client-ledger-service Выше приведен веб-сервис главной книги источников событий. https://github.com/andrewkkchan/client-ledger-core-db И выше, я использую RDBMS для вычисления состояний, чтобы вы могли пользоваться всеми преимуществами, предоставляемыми RDBMS, такими как поддержка транзакций. https://github.com/andrewkkchan/client-ledger-core-memory И у меня есть другой потребитель для обработки в памяти для обработки пакетов.

Можно было бы утверждать, что фактическое хранилище событий выше все еще живет в Kafka--, поскольку СУБД медленна для вставки, особенно когда вставка всегда добавляется.

Я надеюсь, что код поможет вам проиллюстрировать отличные теоретические ответы, уже предоставленные на этот вопрос.