Использование СУБД в качестве хранилища источников событий - PullRequest
102 голосов
/ 15 августа 2011

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

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

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

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

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

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

Ответы [ 5 ]

99 голосов
/ 15 августа 2011

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

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

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-полем "Payload".Это почти то же самое, что и в Ncqrs, только в том, что он сериализует свойства события в двоичном формате (который, например, добавляет поддержку шифрования).

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

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

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]
7 голосов
/ 07 августа 2017

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

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

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

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

Мы остановились на одном контейнере / таблице / коллекции дляпричины ремонтопригодности, но мы поиграли с отдельной таблицей для каждой сущности / объекта.На практике мы обнаружили, что это означает, что либо приложению необходимы разрешения «СОЗДАТЬ» (что, вообще говоря, не очень хорошая идея ... как правило, всегда есть исключения / исключения), либо каждый раз, когда новый объект / объект появляется или развертывается, новыйконтейнеры для хранения / столы / коллекции должны быть сделаны.Мы обнаружили, что это было очень медленно для локальной разработки и проблематично для развертывания производства.Вы не можете, но это был наш реальный опыт.

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

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

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

3 голосов
/ 20 июля 2013

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

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

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

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

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

1 голос
/ 13 марта 2015

Возможный совет - дизайн, за которым следует «Медленно изменяющийся размер» (type = 2), который поможет вам покрыть:

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

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

0 голосов
/ 28 июня 2019

Я думаю, что решение (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 и создавать с минимально необходимыми полямидля вашего случая использования.

...