Використання RDBMS як сховища джерел подій


119

Якщо я використовував RDBMS (наприклад, SQL Server) для зберігання даних джерел подій, як може виглядати схема?

Я бачив кілька варіацій, про які говорили в абстрактному сенсі, але нічого конкретного.

Наприклад, скажімо, що в них є суб'єкт "Продукт", і зміни до цього товару можуть відбуватися у вигляді: ціни, вартості та опису. Я розгублений, чи:

  1. Майте таблицю "ProductEvent", у якій є всі поля для продукту, де кожна зміна означає новий запис у цій таблиці, плюс "хто, що, де, чому, коли і як" (WWWWWH), як це доречно. Коли вартість, ціна або опис змінено, додається цілий новий рядок, який представляє Продукт.
  2. Зберігайте вартість, ціну та опис продукту в окремих таблицях, приєднаних до таблиці продуктів із зовнішнім ключем. Коли відбудуться зміни цих властивостей, запишіть нові рядки за допомогою WWWWWH.
  3. Зберігайте WWWWWH, плюс серіалізований об'єкт, що представляє подію, у таблиці "ProductEvent", тобто сама подія повинна бути завантажена, десериалізована та відтворена в моєму додатку коду, щоб відновити стан програми для даного продукту .

Особливо переживаю варіант 2 вище. Вкрай, таблиця продуктів буде майже однією таблицею на властивість, де для завантаження стану програми для певного продукту потрібно буде завантажити всі події для цього продукту з кожної таблиці подій продукту. Цей стіл-вибух мені пахне неправильно.

Я впевнений, що "це залежить", і хоча немає єдиної "правильної відповіді", я намагаюся зрозуміти, що є прийнятним, а що абсолютно неприйнятним. Я також усвідомлюю, що NoSQL може допомогти тут, де події можуть бути збережені в сукупному корені, що означає лише один запит до бази даних, щоб отримати події для відновлення об'єкта, але ми не використовуємо DB NoSQL на Момент, тому я відчуваю себе альтернативою.


2
У найпростішій формі: [Event] {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 впровадження магазину подій Джонатана Олівера основному складається з однієї таблиці під назвою "Здійснює" з полем BLOB "Корисна навантаження". Це майже те саме, що і в Ncqrs, лише те, що воно серіалізує властивості події у двійковому форматі (що, наприклад, додає підтримку шифрування).

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

Схема його прототипічної таблиці "Події" говорить:

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

9
Гарна відповідь! Один з головних аргументів, про який я продовжую читати, щоб використовувати EventSourcing, - це можливість запитувати історію. Як я збираюся створити інструмент звітності, який є ефективним у запитах, коли всі цікаві дані серіалізуються як XML або JSON? Чи є цікаві статті, які шукають рішення на основі таблиці?
Marijn Huizendveld

11
@MarijnHuizendveld ви, мабуть, не хочете запитувати проти самого магазину подій. Найпоширенішим рішенням було б підключити пару обробників подій, які проектують події в звітність або БД. Повтор історії подій проти цих обробників.
Денніс Трауб

1
@Denis Traub дякує за вашу відповідь. Чому б не запитати проти самого магазину подій? Я боюся, що це стане досить безладним / напруженим, якщо нам доведеться повторювати повну історію щоразу, коли ми придумуємо новий випадок BI?
Marijn Huizendveld

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

10
@theBoringCoder Це здається, що у вас є події Sourcing і CQRS плутаються або принаймні пюре в голові. Вони часто зустрічаються разом, але це не одне і те ж. CQRS дозволяє вам розділити свої моделі читання та запису, тоді як події Sourcing ви використовуєте потік подій як єдине джерело істини у вашій програмі.
Брайан Андерсон

7

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

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

Як розробник проекту, я можу поділитися своєю інформацією щодо деяких варіантів, які ми зробили.

По-перше, ми виявили (навіть із унікальними UUID / GUIDs замість цілих чисел) з багатьох причин послідовні ідентифікатори трапляються з стратегічних причин, таким чином, просто ідентифікатор був недостатньо унікальним для ключа, тому ми об'єднали наш основний стовпець ключа ID з даними / тип об'єкта, щоб створити те, що має бути справді (у сенсі вашої програми) унікальним ключем. Я знаю, що деякі люди кажуть, що вам не потрібно зберігати його, але це буде залежати від того, чи є ви «greenfield» або вам доведеться співіснувати з існуючими системами.

Ми трималися з одним контейнером / таблицею / колекцією з міркувань збереження, але ми розігрувались з окремою таблицею за сутністю / об'єктом. Ми виявили на практиці, що або програма потребує дозволу "СТВОРИТИ" (що, як правило, не є гарною ідеєю ... загалом, завжди є винятки / виключення), або кожен раз, коли новий суб'єкт / об'єкт виник або був розгорнутий, новий контейнери / столи / колекції для зберігання, які потрібно зробити. Ми виявили, що це було дуже повільно для місцевого розвитку та проблематичним для виробництва. Ви можете, ні, але це був наш досвід у реальному світі.

Ще слід пам’ятати, що прохання дії X відбутися може призвести до багатьох різних подій, таким чином, знаючи всі події, породжені командою / подією / тим, що коли-небудь корисно. Вони також можуть бути різними типами об'єктів, наприклад, натискання "купити" в кошику для покупок може призвести до запуску облікових записів та зберігання подій. Захоплююча програма може захотіти знати все це, тому ми додали CorrelationId. Це означало, що споживач може запитати про всі події, викликані в результаті їх запиту. Ви побачите це на схемі .

Зокрема, за допомогою SQL ми виявили, що продуктивність справді стала вузьким місцем, якщо індекси та розділи не були використані належним чином. Пам'ятайте, що події потрібно буде передавати в зворотному порядку, якщо ви використовуєте знімки. Ми спробували кілька різних індексів і виявили, що на практиці потрібні деякі додаткові індекси для налагодження програм у реальному світі у виробництві. Знову ви побачите це на схемі .

Інші метадані у виробництві були корисними під час виробничих досліджень, часові позначки давали нам зрозуміти, в якому порядку тривали події проти подій. Це надавало нам деяку допомогу в особливо важких системах подій, які піднімали величезну кількість подій, даючи нам інформацію про продуктивність таких речей, як мережі та розподіл систем по всій мережі.


Це чудово, дякую. Як це буває, давно, коли я писав це питання, я створив декілька себе як частину своєї бібліотеки Inforigami.Regalo на github. Реалізації RavenDB, SQL Server та EventStore. Задумався, як зробити файл на основі файлів, для сміху. :)
Ніл Барнвелл

1
Ура. Відповідь я додав, головним чином, для інших, хто стикається з нею останнім часом і ділиться деякими вивченими уроками, а не лише результатом.
cdmdotnet

3

Добре, ви можете поглянути на Datomic.

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

Я написав детальну відповідь тут

Ви можете дивитися розмову від Стюарта Хеллоуей, що пояснює дизайн Datomic тут

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


2

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


Я заперечую, що BatchId потенційно може бути пов'язаний з CorrelationId та CausationId. Використовується для з'ясування причин, що спричинили події, та з'єдную їх, якщо потрібно.
Даніель Парк

Це може бути. Однак це так, Було б сенс запропонувати спосіб його налаштування (наприклад, встановлення як ідентифікатора запиту), але фреймворк цього не робить.
Фабіо Марреко

1

Можливий натяк - це дизайн, за яким слід "Повільно змінюючи розмір" (тип = 2), щоб допомогти вам охопити:

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

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


1

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

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 У мене є ще один споживач, який обробляє в пам'яті, щоб обробляти вибухи.

Можна стверджувати, що фактичний магазин подій вище натюрмортів у Кафці - оскільки RDBMS повільний для вставки, особливо коли вставка завжди додається.

Я сподіваюся, що код допоможе надати вам ілюстрацію, окрім дуже хороших теоретичних відповідей, вже наданих на це питання.


Дякую. Я давно створив реалізацію на основі SQL. Я не впевнений, чому RDBMS повільний для вставок, якщо ви десь не зробили неефективний вибір кластерного ключа. Додаток - лише добре.
Ніл Барнвелл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.