User Tools

Site Tools


technology:es

This is an old revision of the document!


Event Sourcing

In Event Sourcing wird der Zustand der Anwendung darüber gespeichert, indem die Ereignisse, die in der Domäne passiert sind, gespeichert werden. Dafür werden sinnvolle und sprechende Ereignisse definiert, und zwar in der Sprache der Domäne. Wenn man die INSERT, UPDATE und DELETE Befehle in der Datenbank loggt, hat man zwar pro Tabelle1) eine Historie über das, was passiert ist, aber man weiß nicht warum. Ein oft genanntes Beispiel ist ein Adresswechsel. Im UPDATE Befehl steht nicht drin, warum die Adresse geändert wurde. Umzug oder Korrektur? Wenn man INSERT, UPDATE und DELETE Befehle speichert, wendet man die Sprache der Datenbankdomäne an, nicht aber der Domäne, die man abbilden möchte.

Ein Adresswechsel einer Person könnte durch AddressCorrected und PersonMoved dargestellt werden. Beide Ereignisse haben zur Folge, dass die Anschrift der Person geändert wird. Man erkennt aber aus den Ereignissen nicht nur was tatsächlich passiert ist, sondern auch warum es passiert ist - zumindest so detailliert wie es die Domäne verlangt2).

Wenn ein Aggregate seinen Zustand durch einen Methodenaufruf ändert, wird die Änderung durch den Methodenaufruf ApplyChange(DomainEvent @event) fest gehalten. Das Aggregate hält seinen Zustand also nicht über eine Sicht auf die aktuellen Eigenschaften, sondern darüber, was alles passiert ist. So wird der Zustand auch gespeichert - die Ereignisse werden in ein EventStore geschrieben. Wenn das Aggregate wieder geladen wird, werden ihm alle seine historischen Ereignisse übergeben, aus denen es sich seinen aktuellen Zustand selbst errechnen kann.

Ein Ereignis hat immer einen Typ der von DomainEvent abgeleitet ist. Zudem hat es die ID des Aggregates zu dem es gehört, und einen laufenden, lückenlosen Zähler3), der die Reihenfolge darstellt4). Konflikte durch gleichzeitigen Zugriff sind einfach zu erkennen. Wenn zwei Prozesse ein Aggregate mit der aktuellen Version = N laden, und beide neue Ereignisse anfügen wollen, würden beide z.B. ein Ereignis mit der Version N+1 anfügen wollen. Die Speicheraktion des zweiten Prozesses wird fehl schlagen, weil auf die ID+Version ein eindeutiger Index eingestellt ist.

Die Wahrheit steckt also in den Ereignissen. Auch das ist ein oft genanntes Argument für Event Sourcing. Man hat nicht nur automatisch ein Log, das Log stimmt mit 100%er Sicherheit mit dem überein, was in der Domäne aktuell los ist. Das aus dem einfachen Grund, weil der aktuelle Zustand sich gerade aus diesem Log errechnet. Es gibt nicht wie üblich einen gespeicherten Zustand und ein separates Log. Da kann es schnell passieren, dass die Beiden auseinander laufen - vielleicht aus dem einfachen Grund dass eine Änderung an einer Eigenschaft nicht mit geloggt wurde. Was dann? Wer hat Recht? Das ist nicht auflösbar. Wenn es in den Ereignissen des Event Sourcing Fehler gibt, dann gibt es sie im aktuellen Zustand und im Log gleichermaßen.

Wenn es zu viele Ereignisse gibt, so dass das Laden eines Aggregates über die historischen Ereignisse zu aufwendig wird, kann man Snapshots einführen. Ein Snapshotter läuft parallel, greift die Ereignisse ab, und macht daraus einen Zustand. Wenn man z.B. die Authentifizierung mit Event Sourcing umsetzt, könnten die letzten 1.000 Loginversuche zu einem Zustand mit { ID = 555, Version = 1.025, Fehlversuche = 2, … } zusammengeführt werden. Wenn das Aggregate nun geladen wird, wird das Snapshot geladen. Dann werden dazu alle Ereignisse mit einer Version > 1.025 dazu geladen5) und dem Aggregate mitgeteilt. Sollte der Snapshotter fehlerhaft sein, muss der Fehler korrigiert werden. Die Snapshots werden einfach verworfen, und er fängt für alle Aggregates mit der Version 1 an. Das System merkt davon nichts, außer, dass die Aggregates anfangs langsamer geladen werden, weil es noch keine aktuellen Snapshots gibt. Es muss aber nichts korrigiert werden, weil die Ereignisse die einzige Quelle der Wahrheit sind. Fehler können immer passieren, das ändert aber nichts an der Tatsache, dass alles, was in den Ereignissen steht, offiziell auch so passiert ist.

Das bedeutet auch, dass das Aggregate, wenn es seinen Zustand aus den historischen Ereignissen herstellt, keinen Fehler werfen kann. Die Ereignisse sind passiert. Wenn sie aus einem Fehlerhaften Grund passiert sind, und der Code für das Aggregate so korrigiert wurde, dass dieser Fehler nicht mehr passieren kann, dann bedeutet das, dass dieser Fehler in Zukunft nicht mehr passieren kann, nicht aber, dass die vergangenen Fehler korrigiert sind. Das Aggregate muss somit weiter machen, auch wenn die historischen Ereignisse keinen Sinn ergeben. Wie, das muss der Entwickler, zusammen mit dem Domänenexperten entscheiden. Zudem kann man den Fehler kompensieren, in dem mann alle Aggregates, die einen fehlerhaften Zustand haben, über einen entsprechenden Methodenaufruf korrigiert. Das Aggregate hat dann Ereignisse, die es fehlerhaft machen, plus weitere Ereignisse, die es wieder in einen nicht-fehlerhaften Zustand versetzen.

Ein Aggregate hat auch einen aktuellen Zustand der dem Anwender angezeigt werden soll. Wenn ich eine Mitarbeiterliste aufrufe, werde ich nicht alle Ereignisse aller Mitarbeiter laden um die Liste anzuzeigen. Das funktioniert ähnlich wie die Snapshots. Es gibt Event Handler, die die veröffentlichte Ereignisse abfangen und daraus denormalisierte Listen erstellen. Man kann für jede UI Sicht eine eigene Liste bauen, d.h. man muss nicht jede Sicht aus Tabellen und JOINS bauen. Man kann die Ereignisse so weiter verarbeiten wie es einem beliebt, so dass auch die kompliziertesten Sichten als einfache, flache, schnell abrufbare Listen aufrufbar sind. Diese Listen, die durch sogenannten Denormalisierungen erzeugt werden, nennt man in der Regel Projektionen, da sie die Ereignisse, die in der Domäne passiert sind, projezieren.

Eventual Consistency

Die Denormalisierer, die die Projektionen bauen, greifen veröffentlichte und gespeicherte Ereignisse ab, und erstellen daraus Listen. Es kann beliebig viele solcher Denormalisierer geben. Manch komplexe Liste kann aufwendig zu bauen sein. Damit das Speichern von Ereignissen nicht durch die Denormalisierer verlangsamt wird, werden erst die Ereignisse gespeichert, und dann in einem anderen Prozess6) von den Event Handlern denormalisiert.

Das bedeutet nun, dass die denormalisierten Listen nicht transaktional mit den Ereignissen gespeichert werden. Wenn ich die Adresse einer Person ändere, und die Aktion erfolgreich durchgeführt wurde, kann es sein, dass die Adressliste, die ich gleich danach aufrufe, noch die alte Adresse anzeigt, weil die neue noch nicht vom Event Handler behandelt wurde. Die Änderung wird schlussendlich in der Liste landen, weil sie ja passiert ist, nur ist nicht klar, wie lange das dauert7). Das ganze läuft unter dem Namen eventual consistency8).

Das ist ziemlich übel, vor allem wenn der Anwender das anders gewohnt ist. Es kann dazu führen, dass der Anwender dem System nicht vertraut, die Änderung wieder und wieder angibt, usw. Die am häufigsten genannte Lösung ist die, dass man den Anwender erziehen muss. Das sehe ich nur bedingt so. Eine andere ist es, nach dem Ergebnis der letzten Aktion zu pollen9), bis es verfügbar ist10). Ich löse es wie folgt:

  • Da wo es geht, denormalisiere ich die Ereignisse transaktional in einfache Listen. Die meisten UI-Sichten können aus diesen einfachen Listen traditionell über JOINs gebaut werden und sind somit immer auf dem aktuellsten Stand,
  • Da, wo das nicht möglich ist, vor allem in komplexen Reports, wird nach dem Ergebnis des Befehls gepollt bevor das UI die Sanduhr abschaltet und die Datenaktion beendet. Wenn nun in eine Listenansicht gewechselt wird, beinhaltet die Liste die gerade gespeicherten Änderungen. Bricht das Pollen mit einem Timeout ab, wird das dem User mitgeteilt - oder aber nicht, siehe nächsten Punkt,
  • Der Denormalisierer speichert zu jeder Liste das Datum und die Uhrzeit der letzten Denormalisierung. Dieses kann in der Liste angezeigt werden, z.B. “Diese Liste enthält alle Änderungen die bis zum 09.01.2013 20:46:45 eingetragen wurden”. Wenn der Anwender weiß, dass er um 20:50 eine Änderung eingetragen hat, kann er die Liste so lange aktualisieren, bis die Listenaktualität mit dem letzten Änderungszeitpunkt übereinstimmt. In Reports sollte das genauso gemacht werden.
1) die in etwa einem Business Objekt entspricht
2) Warum die Person umgezogen ist könnte z.B. auch noch in das PersonMoved Ereignis mit aufgenommen werden, falls das für die Domäne relevant sein sollte
3) die Version des Aggregates
4) die Reihenfolge gilt immer nur pro Aggregate
5) der Snapshotter läuft in einem eigenen Prozess, d.h. wenn ein Aggregate geladen wird, hat der Snaphshotter eventuell noch nicht die neuesten Ereignisse abgearbeitet
6) oder Thread
7) i.d.R. ein paar Millisekunden, wenn der Denormalisierer aber gerade nicht am Start ist, vielleicht deutlich länger
8) schlussendliche Konsistenz
9) man gibt dem Befehl eine PollingId mit, nach der man dann pollen kann
10) denormalisiert wurde
technology/es.1357761614.txt.gz · Last modified: 2013/01/09 21:00 by rtavassoli