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


119

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

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

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

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

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

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


2
В наиболее простой форме: [Событие] {AggregateId, AggregateVersion, EventPayload}. Нет необходимости в агрегатном типе, но вы МОЖЕТЕ сохранить его при желании. Тип события не требуется, но вы МОЖЕТЕ сохранить его при желании. Это длинный список того, что произошло, все остальное - просто оптимизация.
Ив Рейнхаут

7
Определенно держитесь подальше от №1 и №2. Сериализируйте все до капли и храните в таком виде.
Джонатан Оливер

Ответы:


109

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

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

Схема его прототипной таблицы «События» гласит:

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

9
Хороший ответ! Один из основных аргументов, которые я продолжаю читать об использовании EventSourcing, - это возможность запрашивать историю. Как я собираюсь создать инструмент отчетности, который будет эффективно выполнять запросы, когда все интересующие данные сериализуются как XML или JSON? Есть ли какие-нибудь интересные статьи о решении на основе таблиц?
Marijn Huizendveld 06

11
@MarijnHuizendveld вы, вероятно, не захотите запрашивать само хранилище событий. Наиболее распространенное решение - подключить пару обработчиков событий, которые проецируют события в базу данных отчетов или BI. Воспроизвести историю событий для этих обработчиков.
Деннис Трауб

1
@ Денис Трауб благодарит за ответ. Почему бы не запросить само хранилище событий? Боюсь, это станет довольно запутанным / напряженным, если нам придется воспроизводить полную историю каждый раз, когда мы придумываем новый кейс BI?
Marijn Huizendveld

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

10
@theBoringCoder Похоже, вы перепутали Event Sourcing и CQRS или, по крайней мере, запутались в вашей голове. Их часто можно найти вместе, но это не одно и то же. В CQRS вы разделяете модели чтения и записи, а в Event Sourcing вы используете поток событий в качестве единственного источника достоверной информации в вашем приложении.
Брайан Андерсон

7

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

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

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

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

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

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

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

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


Отлично, спасибо. Так получилось, что с тех пор, как я написал этот вопрос, я создал несколько сам как часть моей библиотеки Inforigami.Regalo на github. Реализации RavenDB, SQL Server и EventStore. Интересно, для смеха, сделать файл на основе файлов. :)
Нил Барнуэлл

1
Приветствия. Я добавил ответ в основном для тех, кто сталкивался с ним в последнее время и делится некоторыми извлеченными уроками, а не только результатом.
cdmdotnet 07

3

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

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

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

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

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


2

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

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


Я бы сказал, что BatchId потенциально может быть связан с CorrelationId и CausationId. Используется, чтобы выяснить, что вызвало события, и при необходимости связать их вместе.
Дэниел Парк,

Возможно. Однако это так, имеет смысл предоставить способ его настройки (например, установка идентификатора запроса), но фреймворк этого не делает.
Фабио Марреко,

1

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

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

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


1

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

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

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

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


Спасибо. Я давно построил реализацию на основе SQL. Я не уверен, почему СУБД работает медленно для вставок, если вы не сделали неэффективный выбор для кластеризованного ключа. Только добавление должно быть в порядке.
Нил Барнуэлл,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.