Verwenden eines RDBMS als Event-Sourcing-Speicher


119

Wie könnte das Schema aussehen, wenn ich ein RDBMS (z. B. SQL Server) zum Speichern von Ereignisbeschaffungsdaten verwenden würde?

Ich habe einige Variationen gesehen, über die abstrakt gesprochen wurde, aber nichts Konkretes.

Angenommen, man hat eine "Produkt" -Entität, und Änderungen an diesem Produkt können in Form von: Preis, Kosten und Beschreibung erfolgen. Ich bin verwirrt darüber, ob ich:

  1. Haben Sie eine "ProductEvent" -Tabelle, die alle Felder für ein Produkt enthält, wobei jede Änderung einen neuen Datensatz in dieser Tabelle bedeutet, sowie "wer, was, wo, warum, wann und wie" (WWWWWH). Wenn Kosten, Preis oder Beschreibung geändert werden, wird eine ganz neue Zeile hinzugefügt, um das Produkt darzustellen.
  2. Speichern Sie Produktkosten, Preis und Beschreibung in separaten Tabellen, die mit einer Fremdschlüsselbeziehung mit der Produkttabelle verknüpft sind. Wenn Änderungen an diesen Eigenschaften auftreten, schreiben Sie gegebenenfalls neue Zeilen mit WWWWWH.
  3. Speichern Sie WWWWWH sowie ein serialisiertes Objekt, das das Ereignis darstellt, in einer "ProductEvent" -Tabelle. Dies bedeutet, dass das Ereignis selbst in meinem Anwendungscode geladen, de-serialisiert und erneut abgespielt werden muss, um den Anwendungsstatus für ein bestimmtes Produkt neu zu erstellen .

Besonders mache ich mir Sorgen um Option 2 oben. Im Extremfall wäre die Produkttabelle fast eine Tabelle pro Eigenschaft. Wenn der Anwendungsstatus für ein bestimmtes Produkt geladen werden soll, müssen alle Ereignisse für dieses Produkt aus jeder Produktereignistabelle geladen werden. Diese Tischexplosion riecht für mich falsch.

Ich bin sicher, "es kommt darauf an", und obwohl es keine einzige "richtige Antwort" gibt, versuche ich, ein Gefühl dafür zu bekommen, was akzeptabel und was überhaupt nicht akzeptabel ist. Mir ist auch bewusst, dass NoSQL hier helfen kann, wo Ereignisse gegen einen aggregierten Stamm gespeichert werden können, dh nur eine einzige Anforderung an die Datenbank, um die Ereignisse zum erneuten Erstellen des Objekts abzurufen, aber wir verwenden keine NoSQL-Datenbank an der Moment, also suche ich nach Alternativen.


2
In seiner einfachsten Form: [Event] {AggregateId, AggregateVersion, EventPayload}. Der Aggregattyp ist nicht erforderlich, Sie können ihn jedoch optional speichern. Der Ereignistyp ist nicht erforderlich, Sie können ihn jedoch optional speichern. Es ist eine lange Liste von Dingen, die passiert sind, alles andere ist nur Optimierung.
Yves Reynhout

7
Halten Sie sich auf jeden Fall von # 1 und # 2 fern. Serialisieren Sie alles zu einem Blob und speichern Sie es auf diese Weise.
Jonathan Oliver

Antworten:


109

Der Ereignisspeicher sollte nicht über die spezifischen Felder oder Eigenschaften von Ereignissen informiert sein müssen. Andernfalls würde jede Änderung Ihres Modells dazu führen, dass Ihre Datenbank migriert werden muss (genau wie bei einer guten, altmodischen, zustandsbasierten Persistenz). Daher würde ich Option 1 und 2 überhaupt nicht empfehlen.

Unten sehen Sie das in Ncqrs verwendete Schema . Wie Sie sehen können, speichert die Tabelle "Ereignisse" die zugehörigen Daten als CLOB (dh JSON oder XML). Dies entspricht Ihrer Option 3 (Nur dass es keine "ProductEvents" -Tabelle gibt, da Sie nur eine generische "Events" -Tabelle benötigen. In Ncqrs erfolgt die Zuordnung zu Ihren Aggregate Roots über die "EventSources" -Tabelle, wobei jede EventSource einer tatsächlichen Tabelle entspricht Gesamtwurzel.)

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

Der SQL-Persistenzmechanismus der Event Store-Implementierung von Jonathan Oliver besteht im Wesentlichen aus einer Tabelle mit dem Namen "Commits" und einem BLOB-Feld "Payload". Dies ist so ziemlich das Gleiche wie in Ncqrs, nur dass die Eigenschaften des Ereignisses im Binärformat serialisiert werden (wodurch beispielsweise Verschlüsselungsunterstützung hinzugefügt wird).

Greg Young empfiehlt einen ähnlichen Ansatz, der auf Gregs Website ausführlich dokumentiert ist .

Das Schema seiner prototypischen "Ereignisse" -Tabelle lautet:

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

9
Gute Antwort! Eines der Hauptargumente, über die ich immer wieder lese, um EventSourcing zu verwenden, ist die Möglichkeit, den Verlauf abzufragen. Wie erstelle ich ein Berichterstellungstool, das effizient abfragt, wenn alle interessanten Daten als XML oder JSON serialisiert sind? Gibt es interessante Artikel, die nach einer tabellenbasierten Lösung suchen?
Marijn Huizendveld

11
@MarijnHuizendveld Sie möchten wahrscheinlich nicht nach dem Ereignisspeicher selbst abfragen. Die häufigste Lösung besteht darin, einige Ereignishandler anzuschließen, die die Ereignisse in eine Berichts- oder BI-Datenbank projizieren. Die Wiedergabe des Ereignisverlaufs gegen diese Handler.
Dennis Traub

1
@ Denis Traub danke für deine Antwort. Warum nicht den Ereignisspeicher selbst abfragen? Ich fürchte, es wird ziemlich chaotisch / intensiv, wenn wir jedes Mal, wenn wir einen neuen BI-Fall entwickeln, den gesamten Verlauf wiederholen müssen.
Marijn Huizendveld

1
Ich dachte, irgendwann sollten Sie neben dem Ereignisspeicher auch Tabellen haben, um Daten aus dem Modell in seinem neuesten Zustand zu speichern? Und dass Sie das Modell in ein Lesemodell und ein Schreibmodell aufteilen. Das Schreibmodell widerspricht dem Ereignisspeicher, und die Martials des Ereignisspeichers werden auf das Lesemodell aktualisiert. Das Lesemodell enthält die Tabellen, die die Entitäten in Ihrem System darstellen. Sie können also das Lesemodell verwenden, um Berichte zu erstellen und anzuzeigen. Ich muss etwas falsch verstanden haben.
TheBoringCoder

10
@theBoringCoder Es hört sich so an, als hätten Sie Event Sourcing und CQRS verwirrt oder zumindest im Kopf. Sie werden häufig zusammen gefunden, aber sie sind nicht dasselbe. Mit CQRS können Sie Ihre Lese- und Schreibmodelle trennen, während Sie mit Event Sourcing einen Ereignisstrom als einzige Quelle der Wahrheit in Ihrer Anwendung verwenden.
Bryan Anderson

7

Das GitHub-Projekt CQRS.NET enthält einige konkrete Beispiele dafür, wie Sie EventStores in verschiedenen Technologien ausführen können. Zum Zeitpunkt des Schreibens gibt es eine Implementierung in SQL mit Linq2SQL und ein dazugehöriges SQL-Schema , eine für MongoDB , eine für DocumentDB (CosmosDB, wenn Sie in Azure sind) und eine für EventStore (wie oben erwähnt). In Azure gibt es mehr wie Tabellenspeicher und Blob-Speicher, der dem Flatfile-Speicher sehr ähnlich ist.

Ich denke, der Hauptpunkt hier ist, dass sie alle dem gleichen Auftraggeber / Vertrag entsprechen. Sie alle speichern Informationen an einem einzigen Ort / Container / in einer einzigen Tabelle. Sie verwenden Metadaten, um ein Ereignis von einem anderen zu identifizieren, und speichern das gesamte Ereignis so, wie es war - in einigen Fällen serialisiert, in unterstützenden Technologien, wie es war. Je nachdem, ob Sie eine Dokumentendatenbank, eine relationale Datenbank oder sogar eine Flatfile auswählen, gibt es verschiedene Möglichkeiten, um alle die gleiche Absicht eines Ereignisspeichers zu erreichen (es ist nützlich, wenn Sie Ihre Meinung zu irgendeinem Zeitpunkt ändern und feststellen, dass Sie migrieren oder unterstützen müssen mehr als eine Speichertechnologie).

Als Entwickler des Projekts kann ich einige Einblicke in einige der von uns getroffenen Entscheidungen geben.

Erstens haben wir festgestellt (auch bei eindeutigen UUIDs / GUIDs anstelle von Ganzzahlen), dass aus strategischen Gründen sequenzielle IDs aus strategischen Gründen auftreten. Daher war es für einen Schlüssel nicht eindeutig genug, nur eine ID zu haben. Daher haben wir unsere Haupt-ID-Schlüsselspalte mit den Daten / zusammengeführt. Objekttyp, um einen wirklich (im Sinne Ihrer Anwendung) eindeutigen Schlüssel zu erstellen. Ich weiß, dass einige Leute sagen, dass Sie es nicht speichern müssen, aber das hängt davon ab, ob Sie auf der grünen Wiese sind oder mit vorhandenen Systemen koexistieren müssen.

Wir haben uns aus Gründen der Wartbarkeit an einen einzelnen Container / eine Tabelle / eine Sammlung gehalten, aber wir haben mit einer separaten Tabelle pro Entität / Objekt herumgespielt. Wir haben in der Praxis festgestellt, dass entweder die Anwendung "CREATE" -Berechtigungen benötigt (was im Allgemeinen keine gute Idee ist ... im Allgemeinen gibt es immer Ausnahmen / Ausschlüsse) oder jedes Mal, wenn eine neue Entität / ein neues Objekt existiert oder bereitgestellt wurde, neu Lagerbehälter / Tabellen / Sammlungen mussten gemacht werden. Wir fanden, dass dies für die lokale Entwicklung schmerzlich langsam und für Produktionsbereitstellungen problematisch war. Sie mögen nicht, aber das war unsere reale Erfahrung.

Eine andere Sache, an die Sie sich erinnern sollten, ist, dass das Auffordern von Aktion X dazu führen kann, dass viele verschiedene Ereignisse auftreten, sodass Sie alle Ereignisse kennen, die von einem Befehl / Ereignis / was auch immer nützlich ist, generiert werden. Sie können sich auch auf verschiedene Objekttypen erstrecken, z. B. kann das Drücken von "Kaufen" in einem Einkaufswagen dazu führen, dass Konto- und Lagerereignisse ausgelöst werden. Eine konsumierende Anwendung möchte dies alles wissen, daher haben wir eine Korrelations-ID hinzugefügt. Dies bedeutete, dass ein Verbraucher nach allen Ereignissen fragen konnte, die aufgrund seiner Anfrage ausgelöst wurden. Das sehen Sie im Schema .

Insbesondere bei SQL haben wir festgestellt, dass die Leistung zu einem Engpass wurde, wenn Indizes und Partitionen nicht ausreichend verwendet wurden. Denken Sie daran, dass Ereignisse in umgekehrter Reihenfolge gestreamt werden müssen, wenn Sie Snapshots verwenden. Wir haben einige verschiedene Indizes ausprobiert und festgestellt, dass in der Praxis einige zusätzliche Indizes zum Debuggen von realen Anwendungen in der Produktion erforderlich sind. Das sehen Sie wieder im Schema .

Andere produktionsinterne Metadaten waren bei produktionsbasierten Untersuchungen hilfreich. Zeitstempel gaben uns einen Einblick in die Reihenfolge, in der Ereignisse fortbestanden oder ausgelöst wurden. Dies gab uns Unterstützung bei einem besonders stark ereignisgesteuerten System, das eine große Anzahl von Ereignissen auslöste und uns Informationen über die Leistung von Dingen wie Netzwerken und die Systemverteilung über das Netzwerk gab.


Das ist großartig Danke. Wie es passiert, habe ich seit dem Schreiben dieser Frage einige selbst als Teil meiner Inforigami.Regalo-Bibliothek auf Github erstellt. RavenDB-, SQL Server- und EventStore-Implementierungen. Ich habe mich gefragt, ob ich zum Lachen eine dateibasierte machen soll. :)
Neil Barnwell

1
Prost. Ich habe die Antwort hauptsächlich für andere hinzugefügt, die in jüngerer Zeit darauf gestoßen sind und einige der gewonnenen Erkenntnisse und nicht nur das Ergebnis teilen.
cdmdotnet

3

Vielleicht möchten Sie sich Datomic ansehen.

Datomic ist eine Datenbank mit flexiblen, zeitbasierten Fakten , die Abfragen und Verknüpfungen mit elastischer Skalierbarkeit und ACID-Transaktionen unterstützen.

Ich schrieb eine detaillierte Antwort hier

Sie können einen Vortrag von Stuart Halloway Erklären der Gestaltung von Datomic sehen hier

Da Datomic Fakten rechtzeitig speichert, können Sie sie für Anwendungsfälle zur Ereignisbeschaffung und vieles mehr verwenden.


1

Ein möglicher Hinweis ist, dass das Design gefolgt von "Langsam wechselnde Dimension" (Typ = 2) Ihnen dabei helfen sollte, Folgendes abzudecken:

  • Reihenfolge der Ereignisse (über Ersatzschlüssel)
  • Haltbarkeit jedes Staates (gültig von - gültig bis)

Die Linksfalzfunktion sollte ebenfalls in Ordnung zu implementieren sein, aber Sie müssen über die zukünftige Komplexität der Abfrage nachdenken.


1

Ich denke, Lösung (1 & 2) kann sehr schnell zu einem Problem werden, wenn sich Ihr Domain-Modell weiterentwickelt. Neue Felder werden erstellt, einige ändern ihre Bedeutung und andere können nicht mehr verwendet werden. Schließlich wird Ihre Tabelle Dutzende von nullbaren Feldern haben, und das Laden der Ereignisse wird chaotisch sein.

Denken Sie auch daran, dass der Ereignisspeicher nur für Schreibvorgänge verwendet werden sollte. Sie fragen ihn nur ab, um die Ereignisse zu laden, nicht die Eigenschaften des Aggregats. Sie sind getrennte Dinge (das ist die Essenz von CQRS).

Lösung 3 Was Menschen normalerweise tun, gibt es viele Möglichkeiten, dies zu erreichen.

Beispielsweise erstellt EventFlow CQRS bei Verwendung mit SQL Server eine Tabelle mit diesem Schema:

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
)

wo:

  • GlobalSequenceNumber : Eine einfache globale Identifikation kann zum Bestellen oder Identifizieren der fehlenden Ereignisse beim Erstellen Ihrer Projektion (Lesemodell) verwendet werden.
  • BatchId : Eine Identifikation der Gruppe von Ereignissen, die atomar eingefügt wurden (TBH, habe keine Ahnung, warum dies nützlich wäre)
  • AggregateId : Identifikation des Aggregats
  • Daten : Serialisiertes Ereignis
  • Metadaten : Andere nützliche Informationen aus dem Ereignis (z. B. Ereignistyp, der zum Deserialisieren verwendet wird, Zeitstempel, Absender-ID vom Befehl usw.)
  • AggregateSequenceNumber : Sequenznummer innerhalb desselben Aggregats (dies ist nützlich, wenn keine Schreibvorgänge außerhalb der Reihenfolge ausgeführt werden können. Verwenden Sie dieses Feld daher, um eine optimistische Parallelität zu erzielen.)

Wenn Sie jedoch von Grund auf neu erstellen, würde ich empfehlen, dem YAGNI-Prinzip zu folgen und mit den minimal erforderlichen Feldern für Ihren Anwendungsfall zu erstellen.


Ich würde argumentieren, dass BatchId möglicherweise mit CorrelationId und CausationId zusammenhängt. Wird verwendet, um herauszufinden, was Ereignisse verursacht hat, und um sie bei Bedarf aneinander zu reihen.
Daniel Park

Es könnte sein. Wie auch immer dies ist, es wäre sinnvoll, eine Möglichkeit zum Anpassen bereitzustellen (z. B. Festlegen als ID der Anforderung), aber das Framework tut dies nicht.
Fabio Marreco

1

Ich denke, dies wäre eine späte Antwort, aber ich möchte darauf hinweisen, dass die Verwendung von RDBMS als Event-Sourcing-Speicher durchaus möglich ist, wenn Ihre Durchsatzanforderungen nicht hoch sind. Ich möchte Ihnen nur Beispiele für ein Event-Sourcing-Ledger zeigen, das ich zur Veranschaulichung erstellt habe.

https://github.com/andrewkkchan/client-ledger-service Bei dem oben genannten handelt es sich um einen Event-Sourcing-Ledger-Webdienst. https://github.com/andrewkkchan/client-ledger-core-db Und das oben Genannte Ich verwende RDBMS, um Zustände zu berechnen, damit Sie alle Vorteile einer RDBMS-ähnlichen Transaktionsunterstützung nutzen können. https://github.com/andrewkkchan/client-ledger-core-memory Und ich habe einen anderen Verbraucher, der im Speicher verarbeitet werden muss, um Bursts zu verarbeiten.

Man würde argumentieren, dass der oben genannte tatsächliche Ereignisspeicher immer noch in Kafka lebt - da RDBMS nur langsam eingefügt werden kann, insbesondere wenn das Einfügen immer angehängt wird.

Ich hoffe, der Code hilft Ihnen, eine Illustration zu geben, abgesehen von den sehr guten theoretischen Antworten, die bereits für diese Frage gegeben wurden.


Vielen Dank. Ich habe längst eine SQL-basierte Implementierung erstellt. Ich bin nicht sicher, warum ein RDBMS für Einfügungen langsam ist, es sei denn, Sie haben irgendwo eine ineffiziente Wahl für einen Clusterschlüssel getroffen. Nur Anhängen sollte in Ordnung sein.
Neil Barnwell
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.