Zwei Schreibvorgänge, die nie auseinanderfallen dürfen
Teil 8 der Serie „Hinter der Karte" — wie eine Bewertung und ihre Nachricht an den Rest des Systems garantiert zusammen gelingen oder zusammen scheitern
Kurzfassung
Jede eingehende Bewertung muss zwei Dinge gleichzeitig auslösen: gespeichert werden — und den Rest des Systems benachrichtigen, dass sie da ist. Stürzt der Prozess genau zwischen diesen beiden Schritten ab, entsteht im schlechtesten Fall eine gespeicherte Bewertung, von der niemand sonst je erfährt, oder eine Benachrichtigung über eine Bewertung, die gar nicht gespeichert wurde. Dieser Artikel zeigt die Herausforderung, drei Lösungswege, die wir gegeneinander abgewogen haben, und das sogenannte Outbox-Muster, für das wir uns entschieden haben.
1. Die Herausforderung
Wenn eine neue Bewertung ankommt, passieren im Idealfall zwei Dinge: Sie wird dauerhaft gespeichert, und ein Ereignis „diese Bewertung ist neu" wird für andere Programmteile hinterlegt — etwa für einen späteren Auswertungslauf. Beide Schritte hängen als ein logisches Ereignis zusammen, sind technisch aber zwei getrennte Aktionen. Genau zwischen diesen zwei Aktionen kann ein Prozess abstürzen, ein Server neu starten, eine Netzwerkverbindung reißen.
Drei Ausgänge sind dabei denkbar, und zwei davon sind Fehler:
- Beides gelingt. Der Wunschfall.
- Die Bewertung wird gespeichert, die Benachrichtigung geht verloren. Die Bewertung existiert, aber kein nachgelagerter Prozess erfährt je davon — ein stiller blinder Fleck.
- **Die Benachrichtigung wird verschickt, die Bewertung wird nie gespeichert.** Ein nachgelagerter Prozess reagiert auf etwas, das im System gar nicht existiert.
Die eigentliche Herausforderung lautet: Wie stellt man sicher, dass immer nur der erste Fall eintritt — ohne dabei ein zweites, kompliziertes System (einen Nachrichtenserver, eine externe Warteschlange) einzuführen, nur um diese eine Garantie zu bekommen?
2. Erwogene Ansätze
| Ansatz | Verworfen, weil |
|---|---|
| Direkt an ein externes Nachrichtensystem senden, sobald die Bewertung im Code vorliegt — parallel zum Speichern | Genau die Lücke aus Kapitel 1 bleibt offen: Speichern und Senden sind zwei unabhängige Netzwerkvorgänge zu zwei verschiedenen Systemen. Ein Absturz zwischen beiden erzeugt zuverlässig einen der beiden Fehlerfälle. |
| Ein separates Nachrichtensystem als zusätzliche Infrastruktur betreiben, das speziell für verlässliche Zustellung gebaut ist | Löst das Zustellungsproblem technisch, aber auf Kosten eines zweiten Systems, das betrieben, überwacht und beim Ausfall separat behandelt werden muss — für ein Produkt in dieser Größe unverhältnismäßig, solange die einfachere Lösung ausreicht. |
| Das Ereignis in derselben Datenbank speichern wie die Bewertung selbst, in derselben Transaktion — und erst danach, in einem zweiten, unabhängigen Schritt, tatsächlich zustellen | Gewählt (bekannt als „Outbox-Muster"). Nutzt genau die „beides oder nichts"-Garantie einer einzelnen Datenbank-Transaktion (siehe Teil 7 dieser Serie) für zwei Dinge, die logisch zusammengehören. |
Die entscheidende Erkenntnis: die Datenbank selbst bietet die Zuverlässigkeits-Garantie bereits — sie muss nur für zwei Dinge statt für eines genutzt werden.
3. Die Lösung, konzeptionell
Statt die Benachrichtigung direkt und sofort an ein externes System zu schicken, wird sie zunächst nur als weiterer Datensatz in derselben Datenbank-Transaktion abgelegt, die auch die Bewertung speichert — mit dem Status „wartet noch". Speichern der Bewertung und Ablegen der Benachrichtigung sind damit technisch eine einzige, unteilbare Aktion: entweder gelingen beide, oder keines von beiden. Ein zweiter, komplett unabhängiger Vorgang läuft danach regelmäßig durch die wartenden Benachrichtigungen und stellt sie tatsächlich zu — und genau dieser zweite Schritt darf scheitern und später erneut versucht werden, ohne dass je etwas verloren geht, weil die wartende Benachrichtigung bereits sicher gespeichert ist.
4. Die technische Lösung im Detail
sequenceDiagram
participant A as Ankommende Bewertung
participant DB as Eine Datenbank-Transaktion
participant O as Warteschlangen-Tabelle
participant D as Zustellungs-Lauf (separat, später)
A->>DB: Speichern (Bewertung + Ereignis „wartet")
Note over DB: Beides gelingt zusammen — oder beides scheitert zusammen
DB->>O: Ereignis liegt sicher, Status "wartet"
Note over O,D: Beliebig viel Zeit kann dazwischen vergehen
D->>O: Bis zu 500 wartende Ereignisse abholen
D->>D: Zustellen versuchen
alt Zustellung gelingt
D->>O: Status "zugestellt"
else Zustellung schlägt fehl
D->>O: Status "fehlgeschlagen" — Stapel läuft trotzdem weiter
end
Note over O: Zugestellte Ereignisse älter als 30 Tage werden aufgeräumt
Schritt 1 — dieselbe Transaktion. Der Speichervorgang für eine Bewertung und das Ablegen ihres Ereignisses laufen über dieselbe Datenbankverbindung, innerhalb derselben Arbeitseinheit aus Teil 7 dieser Serie. Damit gilt die „beides oder nichts"-Garantie einer einzelnen Transaktion automatisch für beide Schritte zusammen — ohne dass dafür irgendeine neue Technik nötig wäre.
Schritt 2 — die Warteschlangen-Tabelle. Jedes Ereignis liegt zunächst mit dem Status „wartet" in einer eigenen Tabelle. Es ist noch nicht zugestellt — aber es ist bereits sicher, weil es Teil derselben Transaktion war, die auch die Bewertung selbst betraf.
Schritt 3 — ein separater, fehlertoleranter Zustellungs-Lauf. In einem zweiten, unabhängigen Schritt holt sich ein Zustellungs-Lauf bis zu 500 wartende Ereignisse auf einmal ab und versucht, sie auszuliefern. Gelingt die Zustellung, wird das Ereignis als „zugestellt" markiert. Scheitert sie, wird das Ereignis als „fehlgeschlagen" markiert — und genau ein misslungenes Ereignis darf niemals den gesamten Stapel stoppen. Diese Regel stammt aus derselben Erfahrung wie der wöchentliche Zusammenfassungs- Versand: ein einzelner problematischer Datensatz darf niemals alle anderen mit sich reißen.
Schritt 4 — Aufräumen. Bereits zugestellte Ereignisse werden nach 30 Tagen entfernt, damit die Warteschlangen-Tabelle nicht unbegrenzt wächst.
Wer heute zustellt. Aktuell ist das interne Betriebsjournal der „Abonnent", an den jedes Ereignis geht — dieselbe Schnittstelle, über die später ein externes Nachrichtensystem angeschlossen werden könnte, ohne dass sich am Speicher- und Warteschlangen-Teil irgendetwas ändern müsste.
5. Ein gemessenes Beispiel
Beim Live-Nachweis der zugrundeliegenden Transaktionsgarantie (siehe Teil 7): zwanzig eingespielte Bewertungen erzeugten zwanzig zugehörige Warteschlangen-Einträge. Nach einem Neustart des Serverprozesses waren weiterhin beide Zahlen identisch — keine Bewertung ohne ihr Ereignis, kein Ereignis ohne seine Bewertung.
6. Grenzen der Methode
- Das Muster garantiert „mindestens einmal zugestellt", nicht „genau einmal". Schlägt die Markierung „zugestellt" nach einer erfolgreichen Zustellung fehl, wird beim nächsten Lauf erneut zugestellt. Jeder Empfänger auf der anderen Seite muss deshalb selbst tolerant gegenüber einer möglichen doppelten Zustellung sein.
- Zwischen dem Speichern eines Ereignisses und seiner tatsächlichen Zustellung liegt immer eine gewisse Verzögerung — das Muster ist für „verlässlich, aber nicht sofort" gebaut, nicht für Echtzeit-Zustellung im Millisekundenbereich.
- Die Warteschlangen-Tabelle lebt in derselben Datenbank wie die eigentlichen Daten — bei sehr hohem Ereignis-Aufkommen würde sie irgendwann zur eigenen Skalierungsfrage. Für die heutige Größenordnung ist das kein Problem.
7. Fazit
Die stärkste Eigenschaft dieser Lösung ist, dass sie kein neues System einführt, um eine Zuverlässigkeits-Garantie zu bekommen — sie nutzt eine Garantie, die die Datenbank ohnehin schon gibt, für zwei Dinge statt für eines. Das ist dieselbe Grundidee wie in Teil 7: nicht mehr Technik bauen, sondern die vorhandene Technik so einsetzen, dass die Fehlerklasse strukturell nicht mehr auftreten kann.
Damit endet die zweite Runde dieser Serie — zwei Herausforderungen statt zwei Algorithmen, aber demselben Prinzip verpflichtet: erst die Frage genau stellen, dann erwogene Alternativen ehrlich gegeneinander abwägen, dann die Lösung bauen, die die Fehlerklasse entfernt statt sie nur zu verstecken.
← Teil 7: Der Fehler, der sich hinter 227 grünen Tests versteckte · Zur Übersicht