Read in English · Leer en español · Lire en français
Abschnitt 7.5 von 7.8 · Fortgeschritten · 26 Min. Lesezeit
CQRS und Event-Sourcing
Videolektion
5 Min. · auf Englisch gesprochen, mit UntertitelnNur anhören (Audio)
Transkript lesen
Diese Lektion behandelt zwei Muster, die oft zusammen eingesetzt werden: das Modell, das Daten ändert, von den Modellen zu trennen, die sie lesen, und Daten als Historie von Ereignissen zu speichern. Beide sind mächtig, und beide bringen echte Komplexität mit sich.
Denken Sie an das Hauptbuch einer Bank. Jede Einzahlung und jede Abhebung wird eingetragen, eine Zeile nach der anderen. Nichts wird je ausradiert. Fehler werden durch eine Korrekturzeile behoben. Ihr Kontostand ist einfach die Summe aller Zeilen. Und Ihr monatlicher Kontoauszug ist eine übersichtliche Zusammenfassung aus dem Hauptbuch. Event-Sourcing speichert Daten so wie das Hauptbuch. CQRS hält das Hauptbuch getrennt von den Auszügen.
1988 schlug Bertrand Meyer vor, dass eine Methode entweder den Zustand ändern oder eine Antwort liefern sollte, nicht beides. Um 2010 übertrug Greg Young diese Trennung auf ganze Modelle und nannte sie Command Query Responsibility Segregation, kurz CQRS. Schreiben und Lesen sind verschiedene Aufgaben. Schreiben braucht strenge Regeln, und jede Tatsache wird nur einmal gespeichert. Lesen braucht Geschwindigkeit und Formen, die zu den Bildschirmen passen. Ein Modell, das beides will, macht meist beides schlecht.
So greifen die Teile ineinander, mit Event-Sourcing. Auf der Schreibseite erreicht ein Command, etwa „fünfzig Pfund abheben“, einen Command-Handler. Er lädt das Konto, wendet die Geschäftsregeln an und fügt neue Ereignisse an einen nur anfügbaren Event Store an. Ein Command ist eine Anfrage, also kann es abgelehnt werden. Auf der Leseseite lauscht ein Projektor auf diese Ereignisse und hält Lesemodelle aktuell: hier eine Saldo-Ansicht und eine Auszugsansicht. Queries werden aus diesen Ansichten beantwortet, ohne Joins, ohne Geschäftslogik und ohne Sperren auf der Schreibseite. Und weil Lesemodelle abgeleitet sind, lassen sie sich jederzeit verwerfen und neu aufbauen.
Die meisten Systeme speichern den aktuellen Zustand. Heben Sie Geld ab, ändert sich der Kontostand, und der alte Wert ist weg. Event-Sourcing kehrt das um: Jede Änderung wird als unveränderliches Ereignis in einem nur anfügbaren Log gespeichert. Ereignisse werden in der Vergangenheitsform benannt, etwa „Geld abgehoben“, weil sie bereits akzeptierte Tatsachen festhalten. Ein Ereignis lässt sich nicht ablehnen, nur durch ein späteres ausgleichen. Der aktuelle Zustand ist das, was man durch erneutes Abspielen der Ereignisse der Reihe nach erhält. Die Ereignisse sind die Quelle der Wahrheit; der Kontostand ist eine Berechnung. Dieses Abspielen muss immer dasselbe Ergebnis liefern, also darf es nie eine Uhr oder einen anderen Service aufrufen. Jeder Wert von außen, etwa ein Wechselkurs, wird im Ereignis festgehalten.
Verfolgen wir ein Konto. Das erste Ereignis eröffnet es, ohne Dispositionskredit. Das zweite zahlt fünfhundert Pfund ein. Drei weitere Ereignisse folgen: eine Abhebung von hundertzwanzig Pfund, eine Einzahlung von fünfundsiebzig Pfund fünfzig und eine Abhebung von dreihundert. Der Kontostand beträgt jetzt hundertfünfundfünfzig Pfund fünfzig. Nun versucht die Kundin, zweihundert Pfund abzuheben. Das brächte den Kontostand auf minus vierundvierzig Pfund fünfzig, unter das Limit, also wird das Command abgelehnt und nichts angefügt. Eine Abhebung von zwanzig Pfund ist zulässig. Der Handler fügt Ereignis sechs an und erwartet, dass Version sechs frei ist. War ein anderes Command schneller, schlägt das Anfügen fehl, und er versucht es erneut. Und fragt jemand, warum der Kontostand hundertfünfunddreißig Pfund fünfzig beträgt, sind die sechs Ereignisse die vollständige Antwort.
Dieses Diagramm zeigt denselben Stream. Unter jedem Ereignis steht der laufende Kontostand nach seiner Anwendung, Schritt für Schritt. Fünf Ereignisse zu falten, geht sofort, aber ein belebtes Konto kann Hunderttausende haben. Ein Snapshot speichert den Zustand bei einer bekannten Version, sodass ein Neuaufbau nur die Ereignisse danach anwendet. Ein Snapshot ist ein Cache, nicht die Quelle der Wahrheit. Und Fragen über die Vergangenheit werden einfach. Der Kontostand zu Version drei ist einfach der Fold der ersten drei Ereignisse: dreihundertachtzig Pfund. Keine Historientabelle nötig.
Die Kosten sind real. Laufen Projektionen im Hintergrund, kann ein Nutzer etwas abheben und danach einen Kontostand sehen, der das noch nicht enthält. Ereignisse werden für immer gespeichert, also lässt sich ihr Schema nie einfach ändern. Alte Ereignisse werden beim Lesen in die aktuelle Form umgewandelt, eine Technik namens Upcasting. Personenbezogene Daten aus einem unveränderlichen Log zu löschen, ist schwierig. Eine gängige Technik ist Crypto-Shredding: personenbezogene Felder mit einem Schlüssel pro Kunde verschlüsseln und den Schlüssel vernichten. Ob das als Löschung gilt, ist eine rechtliche Beurteilung. Und Martin Fowler warnt, dass CQRS bei den meisten Systemen riskante Komplexität hinzufügt. Setzen Sie diese Muster für einen Bereich mit wirklich unterschiedlichen Lese- und Schreibanforderungen ein, etwa ein Hauptbuch oder einen Bestelllebenszyklus, nie standardmäßig für ein ganzes System.
Der vollständige Abschnitt enthält den Code, mehr zu Snapshots und Ereignisversionierung sowie eine Entscheidungshilfe. Als Nächstes, in Abschnitt sieben Punkt sechs, geht es um Caching. Bis gleich.
Was Sie wissen müssen
- Command-Query Separation (Bertrand Meyer, 1988) besagt, dass eine Methode entweder den Zustand ändern oder eine Antwort liefern sollte, nicht beides; CQRS (Greg Young, um 2010) wendet dieselbe Trennung auf ganze Modelle an.
- Bei CQRS setzt ein Schreibmodell Geschäftsregeln für Commands durch, während Projektionen ein oder mehrere denormalisierte Lesemodelle für Abfragen aktuell halten; Lesemodelle sind abgeleitete Daten, die sich neu aufbauen lassen.
- Event-Sourcing speichert jede Änderung als unveränderliches Ereignis in der Vergangenheitsform in einem nur anfügbaren Stream pro Aggregat; der aktuelle Zustand ist ein deterministischer Left Fold der Ereignisse.
- Event-Sourcing bietet einen vollständigen Audit-Trail, zeitbezogene Abfragen, neu aufbaubare und rückwirkende Lesemodelle sowie Replay zur Fehlersuche; Snapshots verkürzen lange Folds, sind aber nur ein Cache.
- Die Kosten sind real: Komplexität, Eventual Consistency der Lesemodelle, Versionierung von Ereignissen durch Upcasting oder neue Ereignistypen und das Löschen personenbezogener Daten aus einem unveränderlichen Log, meist durch Crypto-Shredding.
- Martin Fowler warnt, dass CQRS bei den meisten Systemen riskante Komplexität hinzufügt; wenden Sie beide Muster auf bestimmte Bounded Contexts mit wirklich unterschiedlichen Lese- und Schreibanforderungen an, nie standardmäßig auf ein ganzes System.
Von der Command-Query Separation zu CQRS
Die Idee fängt klein an. In Object-Oriented Software Construction (erste Auflage 1988) schlug Bertrand Meyer die Command-Query Separation vor: Jede Methode sollte entweder ein Command sein, das den Zustand ändert und nichts zurückgibt, oder eine Query, die eine Antwort liefert und nichts ändert. Eine Frage zu stellen, sollte nie die Antwort verändern. Martin Fowler weist darauf hin, dass die Regel sinnvolle Ausnahmen hat (ein Element von einem Stack zu nehmen, ändert den Stack und gibt zugleich das Element zurück), aber als Standard macht sie Code leichter nachvollziehbar, weil Queries frei aufgerufen werden können, in beliebiger Reihenfolge und ohne Seiteneffekte.
Um 2010 hob Greg Young das Prinzip von Methoden auf ganze Modelle und nannte das Ergebnis Command Query Responsibility Segregation (CQRS). Statt eines einzigen Objektmodells, und meist eines einzigen Satzes von Tabellen, für Änderungen und Lesezugriffe zugleich hat das System ein Schreibmodell, das Commands verarbeitet und Geschäftsregeln durchsetzt, und ein oder mehrere Lesemodelle, die auf die Fragen der Nutzer zugeschnitten sind. Die Einsicht dahinter: Schreiben und Lesen sind verschiedene Aufgaben. Schreiben braucht strenge Regeln, kleine Transaktionen und eine normalisierte Struktur, damit jede Tatsache nur einmal gespeichert ist. Lesen braucht Geschwindigkeit, denormalisierte Formen, die zu Bildschirmen und Berichten passen, und oft weit mehr Traffic. Ein einzelnes Modell, das beides gut machen will, macht meist beides schlecht, sobald die Domäne reichhaltig wird.
Was CQRS tatsächlich ändert
Auf der Schreibseite kommt ein Command wie WithdrawMoney(account 42, £50) bei einem Command-Handler an. Der Handler lädt das betreffende Aggregat (die Gruppe von Daten, die innerhalb einer Transaktion konsistent bleiben muss, ein Begriff aus dem Domain-Driven Design, behandelt in Abschnitt 5.7), lässt es die Geschäftsregeln anwenden und speichert das Ergebnis. Commands sind Anfragen und können abgelehnt werden: Das Konto kann gesperrt oder die Deckung unzureichend sein. Auf der Leseseite wird eine Query wie Zeige mir die letzten 30 Buchungen mit laufendem Kontostand
aus einem Lesemodell beantwortet, das genau für diesen Bildschirm gebaut ist, ohne Joins, ohne Geschäftslogik und ohne Sperren auf den Schreibtabellen.
Irgendetwas muss die Lesemodelle aktuell halten. Diese Aufgabe übernimmt eine Projektion: Code, der auf Änderungen auf der Schreibseite lauscht und ein Lesemodell aktualisiert. Projektionen können eine Tabelle mit Kontoständen, einen Suchindex der Buchungen, ein Dokument mit dem Monatsauszug und einen Feature Store für die Betrugsbewertung pflegen, jeweils aus demselben Strom von Änderungen. Lesemodelle sind daher abgeleitete Daten: Sie lassen sich jederzeit verwerfen und aus der Schreibseite neu aufbauen. Genau diese Eigenschaft macht es sicher, nächstes Jahr ein neues Lesemodell hinzuzufügen oder die Form eines alten zu ändern.
CQRS gibt es in Abstufungen, und die leichteste Stufe ist oft alles, was ein System braucht. In der leichtesten Form sind Schreib- und Lesemodell verschiedene Klassen über derselben Datenbank, und Lesezugriffe werden über Views oder eigenen Abfragecode bedient. In der mittleren Form liegen die Lesemodelle in eigenen Tabellen, die in derselben Transaktion wie der Schreibvorgang aktualisiert werden. In der schwersten Form liegen die Lesemodelle in anderen Speichern (einer Suchmaschine, einer Dokumentdatenbank, einem Cache) und werden asynchron über Ereignisse aktualisiert, was Skalierung und Flexibilität bringt, aber auch Eventual Consistency. CQRS setzt weder Event-Sourcing noch einen Message Broker noch Microservices voraus; das sind getrennte Entscheidungen, die oft, aber nicht zwingend, zusammen getroffen werden.
CQRS mit Event-Sourcing
Event-Sourcing: Zustand als Folge von Tatsachen
Die meisten Systeme speichern den aktuellen Zustand. Hebt Asha £120 ab, ändert sich die Zeile in accounts von £500 auf £380, und der alte Wert ist weg, sofern nicht jemand zusätzlich eine Audit-Zeile schreibt. Event-Sourcing, 2005 von Martin Fowler beschrieben, kehrt das um: Das System speichert jede Änderung als unveränderliches Ereignis in einem nur anfügbaren Log, und der aktuelle Zustand ist das, was man erhält, wenn man die Ereignisse der Reihe nach erneut abspielt. Die Ereignisse sind die Quelle der Wahrheit; der Kontostand ist eine Berechnung.
Ereignisse werden in der Vergangenheitsform benannt, weil sie Tatsachen festhalten, die bereits geschehen und akzeptiert sind: AccountOpened, MoneyDeposited, MoneyWithdrawn. Ein Command (WithdrawMoney) ist eine Anfrage, die abgelehnt werden kann; ein Ereignis (MoneyWithdrawn) lässt sich nicht mehr ablehnen, nur durch ein späteres Ereignis ausgleichen. Jedes Aggregat hat seinen eigenen Event-Stream, nummeriert ab Version 1, und der Store garantiert, dass die Ereignisse eines Streams in ihrer Reihenfolge bleiben.
Den Zustand neu aufzubauen, ist ein Left Fold: Man beginnt mit nichts, wendet das erste Ereignis an, wendet das zweite auf das Ergebnis an und so weiter. Die Apply-Funktion muss rein und deterministisch sein: Sie darf weder eine Uhr noch einen Zufallszahlengenerator noch einen anderen Service aufrufen, weil das erneute Abspielen derselben Ereignisse immer denselben Zustand ergeben muss. Jeder Wert, der von außen kam (ein Wechselkurs, ein Zeitstempel, eine Betrugsbewertung), wird im Ereignis festgehalten, wenn er zum ersten Mal festgelegt wird.
Das Schreiben folgt einem festen Ablauf. Der Command-Handler lädt den Stream, faltet ihn zum aktuellen Zustand, führt die Geschäftsregel aus und fügt die neuen Ereignisse mit der Version an, die er zu schreiben erwartet. Hat in der Zwischenzeit ein anderes Command an denselben Stream angefügt, schlägt das Anfügen fehl, und der Handler versucht es mit frischem Zustand erneut. Das ist die optimistische Nebenläufigkeitskontrolle aus Abschnitt 7.3, angewendet pro Stream, und deshalb können zwei gleichzeitige Abhebungen nicht beide dieselben £100 ausgeben.
Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: ein Bankkonto als Ereignisse
Ashas Girokonto ist der Stream account-42. Beträge werden in Pence gespeichert, wie Abschnitt 0.2 es für Geld empfiehlt. Fünf Ereignisse wurden angefügt; dann versucht sie, £200 abzuheben.
| v1 AccountOpened (Dispositionslimit £0) | Zustand: Kontostand £0,00, Version 1 |
|---|---|
| v2 MoneyDeposited £500,00 | £0,00 + £500,00 = £500,00, Version 2 |
| v3 MoneyWithdrawn £120,00 | £500,00 − £120,00 = £380,00, Version 3 |
| v4 MoneyDeposited £75,50 | £380,00 + £75,50 = £455,50, Version 4 |
| v5 MoneyWithdrawn £300,00 | £455,50 − £300,00 = £155,50, Version 5 |
| Command: WithdrawMoney £200,00 | Regel: Kontostand minus Betrag darf nicht unter minus das Limit fallen. £155,50 − £200,00 = −£44,50, unter £0. Command abgelehnt; kein Ereignis wird angefügt. |
| Command: WithdrawMoney £20,00 | £155,50 − £20,00 = £135,50 ist zulässig. Der Handler fügt v6 MoneyWithdrawn £20,00 an und erwartet, dass Version 6 frei ist. |
| Zeitbezogene Abfrage: Kontostand nach v3 | Nur v1 bis v3 falten: £380,00. Keine Historientabelle nötig. |
| Audit-Frage: Warum beträgt der Kontostand £135,50? | Die sechs Ereignisse sind die vollständige Antwort, in Reihenfolge, und in jedem ist festgehalten, wer, wann und warum. |
Das abgelehnte Command hat im Stream keine Spur hinterlassen. Muss die Bank Ablehnungen festhalten (etwa für die Betrugsanalyse), fügt sie ein eigenes Ereignis WithdrawalRefused an, das den Kontostand nicht ändert.
Die doppelte Buchführung ist das älteste ereignisbasierte System überhaupt: Buchhalter radieren nie eine Zeile aus, sie buchen eine Korrekturbuchung. Bankdomänen passen daher ganz natürlich zu Event-Sourcing, weshalb es oft an einem Konto erklärt wird.
# A bank account as a stream of events, folded into current state.
# Amounts are in pence so that no floating-point rounding creeps in.
from functools import reduce
events = [
{"v": 1, "type": "AccountOpened", "overdraft_limit": 0},
{"v": 2, "type": "MoneyDeposited", "amount": 50000},
{"v": 3, "type": "MoneyWithdrawn", "amount": 12000},
{"v": 4, "type": "MoneyDeposited", "amount": 7550},
{"v": 5, "type": "MoneyWithdrawn", "amount": 30000},
]
def apply(state, e): # pure: old state + event -> new state
if e["type"] == "AccountOpened":
return {"balance": 0, "limit": e["overdraft_limit"], "version": e["v"]}
sign = 1 if e["type"] == "MoneyDeposited" else -1
return {**state, "balance": state["balance"] + sign * e["amount"], "version": e["v"]}
def decide_withdraw(state, amount): # command side: check the rule, emit events
if state["balance"] - amount < -state["limit"]:
raise ValueError("insufficient funds")
return [{"v": state["version"] + 1, "type": "MoneyWithdrawn", "amount": amount}]
now = reduce(apply, events, None) # {'balance': 15550, 'limit': 0, 'version': 5}
as_at_v3 = reduce(apply, events[:3], None) # temporal query: balance 38000-- An event store can be an ordinary table. The primary key enforces
-- optimistic concurrency: only one writer can append version 6.
CREATE TABLE account_events (
account_id uuid NOT NULL,
version integer NOT NULL,
event_type text NOT NULL,
data jsonb NOT NULL,
recorded_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (account_id, version)
);
-- The command handler read the stream up to version 5, decided, and now appends:
INSERT INTO account_events (account_id, version, event_type, data)
VALUES ('2f9c0d6e-4a51-4c7e-9a43-6d1f2b7e8a10', 6, 'MoneyWithdrawn', '{"amount": 2000}');
-- If a concurrent command already appended version 6, this fails with a
-- unique violation; the handler reloads the stream and decides again.Falten, Snapshots und zeitbezogene Abfragen
Was Event-Sourcing Ihnen bringt
Ein vollständiger Audit-Trail schon durch die Bauweise. In einem zustandsbasierten System ist das Audit-Log ein zweiter Schreibvorgang, den jemand vergessen kann und der den Daten widersprechen kann. In einem System mit Event-Sourcing ist das Log die Daten, also kann es nicht auseinanderlaufen. Aufsichtsbehörden im Bank-, Versicherungs- und Gesundheitswesen schätzen das sehr.
Zeitbezogene Abfragen. Wie hoch war der Kontostand am 31. März um 23:59 Uhr?
oder Welche Bestellungen waren im Status PREPARING, als die Küchenanzeige ausfiel?
werden zu Folds über ein Anfangsstück jedes Streams statt zu forensischer Arbeit an Backups.
Neuaufbau und neue Lesemodelle. Weil Lesemodelle abgeleitet sind, kann ein Team einen Fehler in einer Projektion beheben, deren Ausgabe löschen und alle Ereignisse erneut abspielen, um eine korrigierte Ansicht zu erzeugen. Es kann auch ein Lesemodell bauen, an das beim ersten Aufzeichnen der Ereignisse niemand gedacht hat, etwa einen Bericht darüber, wie oft Kunden innerhalb einer Stunde nach dem Gehaltseingang Bargeld abheben, über die gesamte Historie.
Fehlersuche und Was-wäre-wenn-Analysen. Einen Stream in eine Testumgebung zu kopieren und ihn erneut abzuspielen, reproduziert genau den Zustand, der einen Fehler verursacht hat. Dieselben Ereignisse mit geänderten Regeln abzuspielen, zeigt, was die neuen Regeln getan hätten.
Eine natürliche Quelle für Integrationsereignisse. Die Ereignisse können andere Services speisen, wobei die in einem Stream gespeicherten Ereignisse am besten als internes Detail des besitzenden Service behandelt werden. Sie direkt zu veröffentlichen, koppelt jeden Consumer an das interne Modell; viele Teams übersetzen sie stattdessen in eine kleinere Menge öffentlicher Integrationsereignisse, gestaltet als die langlebigen veröffentlichten Verträge, die Abschnitt 6.4 beschreibt.
Snapshots und lange Streams
Fünf Ereignisse zu falten, geht sofort; 400.000 Ereignisse eines umsatzstarken Händlerkontos bei jedem Command zu falten, nicht. Ein Snapshot speichert den gefalteten Zustand eines Streams bei einer bekannten Version, sodass das Laden zu den neuesten Snapshot lesen und dann nur die Ereignisse danach anwenden
wird. Snapshots sind ein Cache, keine Quelle der Wahrheit: Sie lassen sich jederzeit löschen und im Hintergrund neu erzeugen, ohne das System anzuhalten. Erstellen Sie sie alle N Ereignisse oder nach Zeitplan, und speichern Sie die Version des Apply-Codes mit, denn ein Snapshot aus altem Code ist falsch, sobald sich die Fold-Logik ändert.
Bevor Sie zu Snapshots greifen, prüfen Sie das Stream-Design. Sehr lange Streams bedeuten oft, dass das Aggregat zu groß ist. Ein Bankkonto, das 40 Jahre besteht, lässt sich als Folge kürzerer Streams modellieren, einer pro Auszugszeitraum, jeder eröffnet mit einem Ereignis BalanceCarriedForward, das den Schlusssaldo des vorigen Zeitraums festhält. Das entspricht dem Jahresabschluss in der Buchhaltung und hält jeden Stream kurz genug, um schnell geladen zu werden.
Die Kosten, ehrlich benannt
Komplexität und Ungewohntheit. Event-Sourcing verändert, wie alle Entwickler im Team über Persistenz denken. Einfache Fragen wie Welche Kunden wohnen in Leeds?
lassen sich dem Event Store gar nicht stellen; sie brauchen eine Projektion. Werkzeuge zum Untersuchen, Migrieren und Reparieren von Event Stores sind weniger ausgereift als für relationale Tabellen, und weniger Fachleute haben ein solches System schon in Produktion betrieben.
Eventual Consistency der Lesemodelle. Laufen Projektionen asynchron, kann ein Nutzer eine Abhebung vornehmen und danach einen Kontostand sehen, der sie noch nicht enthält. Die üblichen Abhilfen: die neue Stream-Version vom Command zurückgeben und die Leseseite warten lassen, bis ihre Projektion diese Version verarbeitet hat (eine Read-your-Writes-Garantie wie in Abschnitt 7.4); die Benutzeroberfläche optimistisch aus dem Ergebnis des Commands aktualisieren; oder kritische Lesemodelle synchron in derselben Transaktion wie das Anfügen aktualisieren. Projektionen müssen außerdem idempotent sein und die verarbeitete Position festhalten, weil Ereignisse nach einem Absturz mehr als einmal zugestellt werden können.
Versionierung von Ereignissen. Ereignisse werden für immer gespeichert, also lässt sich ihr Schema nie einfach ändern. Bekommt MoneyDeposited ein Währungsfeld, fehlt es in Jahren alter Ereignisse weiterhin. Zu den Standardtechniken, die Greg Young in Versioning in an Event Sourced System zusammengestellt hat, gehören Tolerant Reader, die für fehlende Felder Standardwerte liefern, Upcasting (eine Funktion, die eine alte Ereignisversion beim Lesen in die aktuelle Form umwandelt, sodass der Fachcode nur eine Version sieht), die Einführung eines neuen Ereignistyps neben dem alten und, als letztes Mittel, das Kopieren eines ganzen Streams in einen neuen mit umgewandelten Ereignissen. Abschnitt 7.7 behandelt die allgemeinen Kompatibilitätsregeln.
Personenbezogene Daten löschen. Ein unveränderliches Log kollidiert mit dem Recht auf Löschung nach dem Datenschutzrecht. Die gängige Technik ist Crypto-Shredding: Personenbezogene Felder in Ereignissen werden mit einem Schlüssel pro Kunde verschlüsselt, der in einem separaten Schlüsselspeicher liegt, und Löschen bedeutet, diesen Schlüssel zu vernichten, sodass der Geheimtext nicht mehr lesbar ist. Die Alternative ist, personenbezogene Daten ganz aus den Ereignissen herauszuhalten und nur einen Verweis auf einen löschbaren Datensatz zu speichern. Ob die Vernichtung eines Schlüssels als Löschung gilt, ist eine rechtliche und keine technische Beurteilung; stimmen Sie den Ansatz daher mit dem Datenschutzbeauftragten (DSB) ab; Abschnitt 8.7 behandelt die weiteren Pflichten.
Betriebsaufwand. Eine Projektion über eine Milliarde Ereignisse neu aufzubauen, kann Stunden dauern; Teams brauchen daher einen Weg, die neue Projektion neben der alten laufen zu lassen und umzuschalten, wenn sie aufgeholt hat. Event Stores wachsen unbegrenzt, und mit ihnen wachsen Speicher-, Backup- und Replay-Zeiten.
Entscheidungshilfe
Entscheidungshilfe: Sollte dieser Teil des Systems CQRS oder Event-Sourcing nutzen?
Stellen Sie diese Fragen für einen Bounded Context, nicht für das ganze System.
- Besteht dieser Bereich überwiegend aus einfachem Anlegen, Lesen, Ändern und Löschen mit wenigen Geschäftsregeln?JaNutzen Sie ein konventionelles Modell mit einer Datenbank. Keines der beiden Muster rechnet sich hier.NeinWeiter: Die Domäne hat echtes Verhalten, das sich zu modellieren lohnt.
- Unterscheiden sich Lese- und Schreibzugriffe stark in Form oder Menge (zum Beispiel viele denormalisierte Bildschirme, Suche oder tausendmal mehr Lese- als Schreibzugriffe)?JaZiehen Sie CQRS in Betracht. Beginnen Sie mit der leichtesten Form: eigener Lesecode oder Views über derselben Datenbank.NeinBehalten Sie ein einziges Modell; ergänzen Sie eine Read Replica oder einen Cache, wenn allein die Leselast das Problem ist.
- Ist die Historie selbst wertvoll: regulatorisches Audit, zeitbezogene Abfragen, erneutes Abspielen mit korrigierter Logik oder viele künftige Lesemodelle aus denselben Tatsachen?JaZiehen Sie Event-Sourcing für diesen Kontext in Betracht, idealerweise in einer hauptbuchartigen Domäne wie Konten, Zahlungen oder Bestelllebenszyklen.NeinSpeichern Sie den aktuellen Zustand; veröffentlichen Sie Ereignisse über eine Outbox, wenn andere Services von Änderungen erfahren müssen.
- Hat das Team die Erfahrung, die Werkzeuge und die Zeit, um Ereignisversionierung, den Neuaufbau von Projektionen und Eventual Consistency zu bewältigen?JaMachen Sie weiter, halten Sie den Bereich mit Event-Sourcing klein, und halten Sie die Entscheidung und ihre Gründe in einem ADR fest.NeinSpeichern Sie den aktuellen Zustand plus eine nur anfügbare Audit-Tabelle; das bringt den größten Teil des Audit-Nutzens zu einem Bruchteil der Kosten.
Wann keines von beiden passt, und häufige Fehler
Keines der beiden Muster passt zu einer Domäne, die überwiegend aus Formularen über Daten besteht, zu einem kleinen Team, das seine erste Version baut, oder zu einem System, dessen einziges Problem die Leselast ist (eine Read Replica oder ein Cache ist weit günstiger). Event-Sourcing passt auch schlecht, wo Daten auf Verlangen in großer Zahl wirklich gelöscht werden müssen oder wo die meisten Abfragen viele Aggregate übergreifen, weil jede solche Abfrage eine eigene Projektion braucht.
Die häufigen Fehler sind vorhersehbar. Teams wenden die Muster systemweit an statt auf einen Kontext. Sie machen gespeicherte Ereignisse zum öffentlichen Vertrag und frieren damit ihr internes Modell ein. Sie bringen abgeleitete Werte oder externe Aufrufe in die Apply-Funktion, sodass das erneute Abspielen andere Antworten liefert. Sie gestalten Ereignisse als generische AccountUpdated-Blobs, die festhalten, dass sich etwas geändert hat, aber nicht, was geschah oder warum, und verlieren damit den größten Teil des Nutzens. Und sie vergessen, dass ein Lesemodell ein Cache ist: Sie fangen an, direkt hineinzuschreiben, und danach lässt es sich nicht mehr neu aufbauen.
Gezielt eingesetzt, ist die Kombination mächtig. Eine Essenslieferplattform könnte den Bestelllebenszyklus mit Event-Sourcing abbilden (aufgegeben, angenommen, in Zubereitung, abgeholt, geliefert, erstattet), weil Streitfälle und Erstattungen die genaue Historie brauchen, und Speisekarten, Restaurantprofile und Nutzereinstellungen als gewöhnliche Tabellen führen. Eine Bank könnte das Hauptbuch und die Zahlungen mit Event-Sourcing abbilden und die Kontaktdaten der Kunden konventionell speichern, was zugleich die meisten personenbezogenen Daten aus dem unveränderlichen Log heraushält.
Prüfen Sie Ihr Verständnis
Warum darf die Funktion, die Ereignisse auf den Zustand anwendet, nie die Systemuhr aufrufen?
Weil das erneute Abspielen derselben Ereignisse immer denselben Zustand ergeben muss. Eine Uhr (oder eine Zufallszahl oder ein externer Aufruf) würde beim Replay andere Antworten liefern; jeder solche Wert muss im Ereignis festgehalten werden, wenn er zum ersten Mal festgelegt wird.
Was ist der Unterschied zwischen dem Command WithdrawMoney und dem Ereignis MoneyWithdrawn?
Das Command ist eine Anfrage, die von den Geschäftsregeln abgelehnt werden kann; das Ereignis ist eine festgehaltene Tatsache, die bereits akzeptiert wurde und sich nur durch ein späteres Ereignis ausgleichen, nie entfernen lässt.
Eine Projektion hat einen Fehler, der drei Monate lang falsche Kontoauszüge erzeugt hat. Wie hilft Event-Sourcing?
Weil das Lesemodell abgeleitet ist, behebt das Team die Projektion, baut durch erneutes Abspielen aller Ereignisse ein neues Lesemodell auf (meist neben dem alten) und schaltet um, ohne Daten von Hand reparieren zu müssen.
Ist ein Snapshot Teil der Quelle der Wahrheit?
Nein. Er ist ein zwischengespeicherter Fold eines Streams bei einer bestimmten Version, der sich jederzeit löschen und aus den Ereignissen neu erzeugen lässt.
Quellen und weiterführende Literatur
Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.
Fragen: „Unser Payments-Team möchte die ganze Plattform mit Event-Sourcing umsetzen. Welche Teile würden wirklich profitieren, und wie würden wir Löschanträge nach der DSGVO behandeln?“