Rehydratisieren von Aggregaten aus einer "Schnappschuss" -Projektion anstelle des Ereignisspeichers

13

Ich habe also schon eine Weile mit Event Sourcing und CQRS geflirtet, obwohl ich nie die Gelegenheit hatte, die Muster auf ein reales Projekt anzuwenden.

Ich verstehe die Vorteile der Trennung von Lese- und Schreibproblemen und weiß zu schätzen, wie mit Event Sourcing Statusänderungen in Datenbanken mit Lesemodell, die sich von Ihrem Event Store unterscheiden, auf einfache Weise projiziert werden können.

Was mir nicht klar ist, warum Sie Ihre Aggregate jemals aus dem Event Store selbst rehydrieren würden.

Wenn das Projizieren von Änderungen an "Lese" -Datenbanken so einfach ist, warum nicht immer Änderungen an einer "Schreib" -Datenbank projizieren, deren Schema perfekt zu Ihrem Domänenmodell passt? Dies wäre effektiv eine Snapshot-Datenbank.

Ich stelle mir vor, dass dies in ES + CQRS-Anwendungen in der Natur ziemlich häufig vorkommt.

Wenn dies der Fall ist, ist der Ereignisspeicher nur dann nützlich, wenn Sie Ihre "Write" -Datenbank aufgrund von Schemaänderungen neu erstellen? Oder vermisse ich etwas Größeres?

MetaFight
quelle
Es ist nichts Falsches daran, asynchron in einen Schreibmodell-Zustandsspeicher zu schreiben und diesen ausschließlich zum Laden von Entitäten zu verwenden. Unabhängig davon, ob Sie dies tun oder nicht, gibt es genau dieselben Konsistenzprobleme. Der Schlüssel zur Behebung dieser Konsistenzprobleme liegt in der unterschiedlichen Modellierung Ihrer Entitäten. Event Sourcing ist nichts Magisches, das diese Konsistenzprobleme löst. Die Magie liegt im Modellieren und nicht im Sorgen. Es gibt bestimmte Anwendungen, für die auf dieser Ebene Konsistenz erforderlich ist und die unabhängig von der Modellierung höchst umstrittene Entitäten aufweisen, und die unabhängig davon besondere Aufmerksamkeit erfordern.
Andrew Larsson
Solange Sie die Zustellung von Veranstaltungen garantieren können. Zu diesem Zweck muss Ihre Anwendung lediglich ein Ereignis synchron auf einem dauerhaften Ereignisbus veröffentlichen. Nach dem Veröffentlichen ist der Job der Anwendung abgeschlossen. Der Bus sendet es dann an verschiedene Ereignishandler: einen zum Aktualisieren des Ereignisspeichers, einen zum Aktualisieren des Zustandsspeichers und alle anderen, die zum Aktualisieren der Lesespeicher erforderlich sind. Der Grund, warum Sie Event Sourcing verwenden, ist, dass Sie sich nicht mehr um die sofortige Konsistenz kümmern. Umarme es.
Andrew Larsson
Es gibt keinen Grund, warum Sie Ihre Entitäten ständig aus dem Ereignisspeicher laden sollten. Das ist nicht ihr Zweck. Ihr Zweck ist es, ein unformatiertes, permanentes Hauptbuch über alles zu erstellen, was im System aufgetreten ist. Entitätszustandsspeicher und denormalisierte Lesemodelle dienen zum Laden und Lesen.
Andrew Larsson

Antworten:

13

Was mir nicht klar ist, warum Sie Ihre Aggregate jemals aus dem Event Store selbst rehydrieren würden.

Weil die "Ereignisse" das Buch der Aufzeichnungen sind.

Wenn das Projizieren von Änderungen an "Lese" -Datenbanken so einfach ist, warum nicht immer Änderungen an einer "Schreib" -Datenbank projizieren, deren Schema perfekt zu Ihrem Domänenmodell passt? Dies wäre effektiv eine Snapshot-Datenbank.

Ja; Manchmal ist es eine nützliche Leistungsoptimierung, eine zwischengespeicherte Kopie des Aggregatzustands zu verwenden, anstatt diesen Zustand jedes Mal von Grund auf neu zu generieren. Denken Sie daran: Die erste Regel zur Leistungsoptimierung lautet "Don't". Die Lösung wird dadurch komplexer, und Sie möchten dies lieber vermeiden, bis Sie eine überzeugende Geschäftsmotivation haben.

Wenn dies der Fall ist, ist der Ereignisspeicher nur dann nützlich, wenn Sie Ihre "Write" -Datenbank aufgrund von Schemaänderungen neu erstellen? Oder vermisse ich etwas Größeres?

Ihnen fehlt etwas Größeres.

Der erste Punkt ist, dass Sie, wenn Sie sich für eine ereignisbasierte Lösung entscheiden, erwarten, dass die Historie des Geschehens erhalten bleibt, dh, Sie möchten zerstörungsfreie Änderungen vornehmen .

Deshalb schreiben wir überhaupt an den Event Store.

Dies bedeutet insbesondere, dass jede Änderung in den Ereignisspeicher geschrieben werden muss.

Konkurrierende Autoren können möglicherweise die Schreibvorgänge des jeweils anderen zerstören oder das System in einen unbeabsichtigten Zustand versetzen, wenn sie sich der Änderungen des anderen nicht bewusst sind. Der übliche Ansatz, wenn Sie Konsistenz benötigen, besteht darin, Ihre Schreibvorgänge an eine bestimmte Position im Journal zu richten (analog zu einem bedingten PUT in einer HTTP-API). Ein fehlgeschlagener Schreibvorgang weist den Schreiber darauf hin, dass sein derzeitiges Verständnis des Journals nicht synchron ist und dass er sich erholen sollte.

Die Rückkehr zu einer bekannten guten Position und die Wiederholung weiterer Ereignisse seit diesem Zeitpunkt ist eine gängige Wiederherstellungsstrategie. Diese bekannte gute Position kann eine Kopie des lokalen Caches oder eine Darstellung in Ihrem Snapshot-Speicher sein.

Auf dem glücklichen Pfad können Sie eine Momentaufnahme des Aggregats im Speicher behalten. Sie müssen sich nur an ein externes Geschäft wenden, wenn keine lokale Kopie verfügbar ist.

Darüber hinaus müssen Sie nicht vollständig eingeholt werden, wenn Sie Zugriff auf das Geschäftsbuch haben.

Daher besteht der übliche Ansatz ( bei Verwendung eines Snapshot-Repositorys) darin, es asynchron zu verwalten . Auf diese Weise können Sie eine Wiederherstellung durchführen, ohne den gesamten Verlauf des Aggregats erneut zu laden und abzuspielen.

In vielen Fällen ist diese Komplexität nicht von Interesse, da feinkörnige Aggregate mit begrenzter Lebensdauer normalerweise nicht genügend Ereignisse erfassen, um die Kosten für die Verwaltung eines Snapshot-Caches zu übersteigen.

Wenn es sich jedoch um das richtige Tool für das Problem handelt, ist es durchaus sinnvoll, eine veraltete Darstellung des Aggregats in das Schreibmodell zu laden und es anschließend mit zusätzlichen Ereignissen zu aktualisieren.

VoiceOfUnreason
quelle
Das klingt alles vernünftig. Als ich vor einiger Zeit mit einer Dummy-Implementierung von ES herumgespielt habe, habe ich den EventStore verwendet, um die Konsistenz zu gewährleisten, aber ich habe auch synchron in ein sogenanntes "Snapshot-Repository" geschrieben. Dies bedeutete, dass der aktuelle Status immer zum Lesen bereit war, ohne dass Ereignisse wiederholt werden mussten. Ich vermutete, dass dies nicht gut skalieren würde, aber da es nur eine Übung war, machte es mir nichts aus.
MetaFight
6

Da Sie nicht angeben, was der Zweck der "Schreib" -Datenbank sein soll, gehe ich hier davon aus, dass Sie Folgendes meinen: Wenn Sie ein neues Update für ein Aggregat registrieren, anstatt das Aggregat aus dem Ereignisspeicher neu zu erstellen Heben Sie es aus der "Write" -Datenbank, überprüfen Sie die Änderung und geben Sie ein Ereignis aus.

Wenn dies so gemeint ist, schafft diese Strategie eine Bedingung für Inkonsistenz: Wenn ein neues Update durchgeführt wird, bevor das letzte Update die Möglichkeit hatte, in die Datenbank "write" zu gelangen, wird das neue Update gegen veraltete Daten validiert. Auf diese Weise wird möglicherweise ein "unmögliches" (dh "unzulässiges") Ereignis ausgegeben und der Systemstatus beschädigt.

Betrachten Sie beispielsweise ein Beispiel für die Buchung von Sitzplätzen in einem Theater. Um Doppelbuchungen zu vermeiden, müssen Sie sicherstellen, dass der gebuchte Sitzplatz nicht bereits belegt ist - dies wird als "Bestätigung" bezeichnet. Dazu hinterlegen Sie eine Liste der bereits gebuchten Plätze in der Datenbank "write". Wenn dann eine Buchungsanfrage eingeht, prüfen Sie, ob der angeforderte Platz in der Liste enthalten ist. Wenn nicht, geben Sie eine "gebuchte" Veranstaltung aus, andernfalls antworten Sie mit einer Fehlermeldung. Anschließend führen Sie einen Projektionsprozess durch, in dem Sie die "gebuchten" Ereignisse abhören und die gebuchten Plätze zur Liste in der Datenbank "Schreiben" hinzufügen.

Normalerweise würde das System so funktionieren:

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Projection process catches the event, adds seat #1 to the "already booked" list.
5. Another request to book seat #1.
6. Check in the list: the list contains seat #1
7. Respond with an error message.

Was ist jedoch, wenn Anforderungen zu schnell eingehen und Schritt 5 vor Schritt 4 erfolgt?

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Another request to book seat #1.
5. Check in the list: the list is still empty.
6. Issue another "booked seat #1" event.

Jetzt haben Sie zwei Termine, um denselben Platz zu buchen. Der Systemstatus ist beschädigt.

Um dies zu verhindern, sollten Sie Aktualisierungen niemals anhand einer Projektion validieren. Um ein Update zu validieren, erstellen Sie das Aggregat aus dem Ereignisspeicher neu und validieren das Update anhand dessen. Danach geben Sie ein Ereignis aus, verwenden jedoch den Zeitstempelschutz, um sicherzustellen, dass seit dem letzten Lesen aus dem Speicher keine neuen Ereignisse ausgegeben wurden. Wenn dies fehlschlägt, versuchen Sie es erneut.

Das Neuerstellen von Aggregaten aus dem Ereignisspeicher kann zu einer Leistungsbeeinträchtigung führen. Um dies zu vermeiden, können Sie aggregierte Snapshots direkt im Ereignisstrom speichern, die mit der ID des Ereignisses versehen sind, aus dem der Snapshot erstellt wurde. Auf diese Weise können Sie das Aggregat neu erstellen, indem Sie den letzten Snapshot laden und nur Ereignisse wiedergeben, die danach aufgetreten sind, anstatt immer den gesamten Ereignisstrom vom Anfang der Zeit an wiederzugeben.

Fyodor Soikin
quelle
Vielen Dank für Ihre Antwort (und entschuldigen Sie, dass Sie so lange brauchen, um zu antworten). Was Sie über die Überprüfung anhand der Schreibdatenbank sagen, ist nicht unbedingt wahr. Wie in einem anderen Kommentar erwähnt, habe ich in einer ES-Beispielimplementierung, mit der ich gespielt habe, sichergestellt, dass meine Schreibdatenbank synchron aktualisiert wird (und die ConcurrencyId / den Zeitstempel gespeichert werden). Dadurch konnte ich Verstöße gegen die optimistische Parallelität erkennen, ohne den EventStore verlassen zu müssen. Zugegeben, synchrone Schreibvorgänge alleine schützen nicht vor Datenbeschädigung, aber ich habe auch Schreibvorgänge mit einem Zugriff (Single-Thread) ausgeführt.
MetaFight
Also hatte ich meine Konsistenzprobleme gelöst. Ich ging jedoch davon aus, dass dies zu Lasten der Skalierbarkeit geht.
MetaFight
Das synchrone Schreiben in die Schreibdatenbank birgt immer noch die Gefahr einer Beschädigung: Was passiert, wenn das Schreiben in den Ereignisspeicher erfolgreich ist, das Schreiben in die Schreibdatenbank jedoch fehlschlägt?
Fyodor Soikin
1
Wenn das Lesen der Projektion fehlschlägt, wird es nur wiederholt, bis es erfolgreich ist. Wenn es vollständig abstürzt, wird es wieder aktiviert und fährt dort fort, wo es abgestürzt ist. Mit anderen Worten, wiederholen Sie den Vorgang. Der von außen wahrnehmbare Effekt würde sich nicht davon unterscheiden, dass er nur ein bisschen langsam läuft. Wenn die Projektion immer wieder ausfällt, bedeutet dies, dass ein Fehler vorliegt, der behoben werden muss. Nach der Korrektur wird der Betrieb ab dem letzten fehlerfreien Zustand fortgesetzt. Wenn die gesamte gelesene Datenbank infolge des Fehlers beschädigt wird, werde ich die Datenbank mit Hilfe des Ereignisprotokolls einfach von Grund auf neu erstellen.
Fyodor Soikin
1
Es gehen nie Daten verloren, das ist der springende Punkt. Daten können für eine Weile in einer unbequemen Form (zum Lesen) stecken bleiben, aber sie gehen nie verloren.
Fyodor Soikin
3

Der Hauptgrund ist die Leistung. Sie können für jedes Commit einen Snapshot speichern (Commit = die Ereignisse, die von einem einzelnen Befehl generiert werden, normalerweise nur ein Ereignis), dies ist jedoch kostspielig. Entlang des Schnappschusses müssen Sie auch das Commit speichern, sonst wäre es kein Event-Sourcing. Und das alles muss atomar gemacht werden, alles oder nichts. Ihre Frage ist nur gültig, wenn separate Datenbanken / Tabellen / Sammlungen verwendet werden (andernfalls wäre dies genau das Ereignis-Sourcing), sodass Sie gezwungen sind, Transaktionen zu verwenden , um die Konsistenz zu gewährleisten. Transaktionen sind nicht skalierbar. Ein Nur-Anhängen-Ereignisstrom (der Ereignisspeicher) ist die Mutter der Skalierbarkeit.

Der zweite Grund ist die Gesamtkapselung. Sie müssen es schützen. Dies bedeutet, dass das Aggregat jederzeit die Möglichkeit haben sollte, seine interne Darstellung zu ändern. Wenn Sie es speichern und stark davon abhängig sind, werden Sie Schwierigkeiten mit der Versionierung haben, was mit Sicherheit passieren wird. In der Situation , wenn Sie den Snapshot als Optimierungs nur verwenden, wenn Schemaänderungen Sie einfach diese Schnappschüsse ignorieren ( einfach ich wirklich nicht so denken, viel Glück bestimmt wird, dass Aggregate von Schemaänderungen - einschließlich aller verschachtelten Einheiten und Wertobjekte - in einem? effiziente Art und Weise und das Management).

Constantin Galbenu
quelle
Wenn sich mein Aggregatschema ändert, ist es dann nicht einfach, meine Ereignisse erneut abzuspielen, um eine aktualisierte "Write" -Datenbank zu erstellen.
MetaFight
Das Problem ist die Erkennung dieser Änderung. Ein Aggregat kann mit vielen Dateien / Klassen sehr groß sein.
Constantin Galbenu
Ich verstehe nicht Die Änderung würde mit einer Softwareversion erfolgen. Die Veröffentlichung würde wahrscheinlich mit einem Datenbankskript zur Neuerstellung der "Schreib" -Datenbank geliefert.
MetaFight
Es ist viel Arbeit für ein Migrationsskript. Während es läuft, muss die App ausgefallen sein.
Constantin Galbenu
@MetaFight Wenn der Stream sehr groß ist, wird es viel Zeit in Anspruch nehmen, das neue Aggregatschema wiederherzustellen. Ich denke jetzt an einen Schnappschuss, der den Status einer Live-Projektion darstellt, die vor der Veröffentlichung des neuen Aggregats ausgeführt werden könnte Schema
Narvalex