Sven Erik Matzen

Software Architect | Cloud & Security Expert | AI-enabled Solutions

Das Versprechen, das keiner halten kann: Zwei-Phasen-Commit, Sagas und das Ende der verteilten Transaktion

🎧 Listen to this article

Software-Architekturen · 2026-10-02

EU-Kennzeichnung: vollständig KI-generierter Inhalt Vollständig KI-generierter Artikel (ohne Vorabprüfung).

Der Aufhänger: Zwei Buchungen, ein Wunsch

Du baust ein Reiseportal. Ein Kunde klickt auf „Buchen", und zwei Dinge sollen passieren: Der Flug wird reserviert, und das Hotelzimmer wird reserviert. Beides oder keins. Ein Flug ohne Hotel ist ärgerlich; ein Hotel ohne Flug ist teuer; und ein Kunde, der für beides bezahlt hat, aber nur eines bekommt, ist ein Supportfall mit Erinnerungsvermögen.

In einer einzigen relationalen Datenbank ist das Problem gelöst, und zwar seit den 1970er Jahren. Du schreibst BEGIN TRANSACTION, führst zwei INSERTs aus, schreibst COMMIT, und die Datenbank garantiert dir mit ihrem Write-Ahead-Log, dass entweder beide Zeilen da sind oder keine – auch dann, wenn genau zwischen den beiden INSERTs der Strom ausfällt. Das ist eine der erfolgreichsten Abstraktionen der Informatik: Du darfst so programmieren, als könne nichts schiefgehen, weil jemand anderes sich darum gekümmert hat, dass alles schiefgehen darf.

Nun liegt der Flugbestand aber in einem anderen System als der Hotelbestand. Zwei Dienste, zwei Datenbanken, zwei Teams, möglicherweise zwei Rechenzentren. Und plötzlich ist die Abstraktion weg. Die naheliegende Reaktion ist: Dann machen wir die Transaktion eben verteilt. Es gibt dafür ein Protokoll, es heißt Zwei-Phasen-Commit, es ist seit fast fünfzig Jahren bekannt, es steht in jedem Lehrbuch, und in .NET gibt es dafür sogar eine Klasse mit drei Zeilen Code drumherum.

Dieser Artikel handelt davon, warum diese naheliegende Reaktion – trotz korrekter Theorie, trotz funktionierender Implementierungen – in verteilten Systemen fast immer die falsche ist. Und warum das kein Versagen der Ingenieure ist, sondern ein mathematisch nachgewiesenes Eigentum der Welt, in der wir programmieren.

Die Kurzfassung: Atomarität über Systemgrenzen hinweg ist nicht unmöglich. Sie ist nur teuer – in Latenz, in Sperrdauer, in Verfügbarkeit – und sie ist blockierend, und zwar beweisbar. Wer sie trotzdem will, muss sie sich mit Konsens erkaufen. Wer sie nicht bezahlen will, muss sein Konsistenzversprechen umschreiben, statt es zu brechen. Beides sind legitime Architekturen. Nur das Dritte – Atomarität behaupten und Sagas bauen – ist eine Lüge, die irgendwann der Support bezahlt.


Teil 1: Was eine Transaktion eigentlich verspricht

Der Transaktionsbegriff, wie wir ihn heute benutzen, wurde maßgeblich von Jim Gray geprägt. In seinen „Notes on Data Base Operating Systems" (1978, erschienen in Operating Systems, Lecture Notes in Computer Science Band 60, Seiten 393–481) beschreibt er das Transaktionsmanagement eines Datenbanksystems – Sperren, Protokollierung, Wiederherstellung – in einer Vollständigkeit, die den Text zum Gründungsdokument einer Disziplin machte. Er selbst nannte es bescheiden „ein Kompendium der Folklore von Datenbank-Betriebssystemen". Genau in dieser Arbeit wird auch das Zwei-Phasen-Commit-Protokoll ausbuchstabiert.

Eine Transaktion verspricht vier Dinge, die man später unter dem Akronym ACID zusammenfasste. Für unseren Zweck sind zwei davon entscheidend:

Atomarität heißt: Die Transaktion ist unteilbar. Es gibt für jeden Beobachter nur zwei Zustände – vorher und nachher. Ein Zwischenzustand, in dem der Flug gebucht und das Hotel nicht gebucht ist, existiert nicht als sichtbarer Zustand der Welt.

Isolation heißt: Während die Transaktion läuft, sieht niemand anderes ihre halbfertigen Effekte. Zwei gleichzeitige Transaktionen verhalten sich so, als wären sie hintereinander gelaufen.

Diese beiden Versprechen sind der Grund, warum Transaktionen so angenehm sind – und sie sind zugleich der Grund, warum sie über Netzwerkgrenzen hinweg so schmerzhaft werden. Atomarität verlangt, dass alle Beteiligten dieselbe Entscheidung treffen. Isolation verlangt, dass Sperren gehalten werden, bis diese Entscheidung gefallen ist. Im lokalen Fall dauert das Mikrosekunden. Über ein Netzwerk dauert es Millisekunden – und manchmal, wie wir sehen werden, unbestimmt lange.


Teil 2: Der Zwei-Phasen-Commit, Schritt für Schritt

Das Protokoll hat zwei Rollen. Ein Transaktionsmanager (TM, oft „Koordinator") und N Resource Manager (RM, die eigentlichen Datenbanken oder Dienste). Es läuft so:

Phase 1 – Vorbereiten. Der Koordinator fragt jeden RM: „Kannst du committen?" Ein RM, der ja sagt, tut damit etwas Schwerwiegendes: Er schreibt alle nötigen Änderungen und Sperren dauerhaft in sein Log und gibt damit ein bindendes Versprechen ab, dass er committen wird, wenn man es ihm sagt – egal was dazwischen passiert, inklusive eines Stromausfalls. Er darf von diesem Moment an nicht mehr einseitig abbrechen. Dieser Zustand heißt prepared.

Phase 2 – Entscheiden und mitteilen. Haben alle RMs „prepared" gemeldet, schreibt der Koordinator die Entscheidung COMMIT dauerhaft in sein eigenes Log – das ist der Moment, in dem die Transaktion unwiderruflich committed ist – und teilt sie allen RMs mit. Hat ein einziger RM „abort" gesagt oder nicht geantwortet, lautet die Entscheidung ABORT.

Gray und Lamport haben die Kosten dieses Ablaufs 2006 präzise aufgeschrieben: Im Normalfall braucht 2PC „insgesamt 3N - 1 Nachrichten" und „vier Nachrichtenverzögerungen", dazu drei erzwungene Schreibvorgänge auf stabilen Speicher – der des ersten vorbereitenden RM, die der übrigen RMs, und der des TM beim Fällen der Entscheidung.

Vier Nachrichtenverzögerungen. Merken wir uns die Zahl: Bei 1 ms Netzwerk-Roundtrip innerhalb eines Rechenzentrums sind das wenige Millisekunden; über Kontinente hinweg sind das leicht 300 ms. Und während dieser ganzen Zeit halten alle beteiligten Systeme ihre Sperren.

Das Fenster der Unsicherheit

Jetzt kommt der Teil, der 2PC seinen schlechten Ruf eingebracht hat. Betrachte einen RM, der „prepared" gemeldet hat und nun auf die Entscheidung wartet. In diesem Zustand gilt:

  • Er darf nicht committen, denn vielleicht hat ein anderer RM abgebrochen.
  • Er darf nicht abbrechen, denn er hat versprochen, committen zu können, und vielleicht hat der Koordinator bereits COMMIT entschieden und anderen mitgeteilt.
  • Er hält seine Sperren, denn er muss sein Versprechen einlösen können.

Dieser Zustand heißt in-doubt. Fällt der Koordinator in diesem Moment aus, wartet der RM. Er wartet, bis der Koordinator wiederkommt und sein Log liest. Er wartet mit gehaltenen Sperren. Und alles, was diese Datensätze anfassen will, wartet mit.

Das ist nicht ein Implementierungsfehler, den man beheben könnte. Es ist die unvermeidliche Konsequenz daraus, dass der RM die Entscheidung nicht selbst treffen darf und die einzige Quelle der Wahrheit nicht erreichbar ist. In der Praxis bedeutet es: Ein 2PC-Koordinator ist ein Single Point of Failure, der nicht nur die Verfügbarkeit, sondern die Schreibbarkeit aller beteiligten Datensätze blockiert.


Teil 3: Die Unmöglichkeit, die man nicht wegoptimieren kann

Dieses Blockieren ließ die Forschung nicht ruhen. Dale Skeen veröffentlichte 1981 auf der ACM-SIGMOD-Konferenz „Nonblocking Commit Protocols" und entwickelte darin den Drei-Phasen-Commit (3PC): Zwischen Vorbereitung und Commit wird eine zusätzliche Phase eingeschoben, ein „pre-commit", in der alle Teilnehmer erfahren, dass committet werden soll, bevor es jemand tut. Nach dieser Mitteilung kann ein überlebender Teilnehmer die Entscheidung selbst herleiten, ohne den Koordinator zu fragen. Das Blockieren beim Koordinatorausfall ist damit beseitigt.

Nur: Es ist beseitigt unter einer Annahme, die in realen Systemen nicht gilt. 3PC setzt ein Netzwerk mit beschränkter Verzögerung und Knoten mit beschränkter Antwortzeit voraus – ein synchrones Systemmodell. Es kostet außerdem eine zusätzliche Nachrichtenrunde und damit Latenz.

Zwei Jahre später lieferten Dale Skeen und Michael Stonebraker mit „A Formal Model of Crash Recovery in a Distributed System" (IEEE Transactions on Software Engineering SE-9(3), Mai 1983) den formalen Rahmen – und das Ergebnis, das die Hoffnung auf ein freundlicheres Protokoll beendete: Wenn das Netzwerk partitionieren kann, also wenn Knoten nicht nur abstürzen, sondern einander zeitweise nicht erreichen können, dann gibt es kein Commit-Protokoll, das gleichzeitig konsistent und blockierungsfrei ist. Irgendjemand muss warten. Immer.

Das ist dieselbe Sorte Resultat wie das FLP-Theorem beim verteilten Konsens (siehe Wie Maschinen sich einig werden: Verteilter Konsens von FLP über Paxos zu Raft): keine Aussage über heutige Technik, sondern eine Grenze des Möglichen. Wer ein verteiltes Commit-Protokoll baut, das in Partitionen nie blockiert und nie inkonsistent wird, hat einen Fehler im Beweis gefunden oder einen Fehler im Code.


Teil 4: Die Rettung durch Konsens – und warum 2PC ein Spezialfall ist

Wenn das Problem der zentrale Koordinator ist, dann liegt die Lösung auf der Hand: Mach den Koordinator hochverfügbar. Genau das haben Jim Gray und Leslie Lamport 2006 in „Consensus on Transaction Commit" (ACM Transactions on Database Systems 31(1), Seiten 133–160) getan – einer meiner Lieblingsarbeiten der Fachliteratur, weil sie zwei Teilgebiete vereinigt, die zwanzig Jahre lang nebeneinander existiert hatten.

Die Idee: Paxos Commit benutzt „eine separate Instanz des Paxos-Konsensalgorithmus, um Einigkeit über die Entscheidung jedes RM zu erzielen, ob er vorbereiten oder abbrechen will". Die Entscheidung liegt nicht mehr im Log eines einzelnen Koordinators, sondern wird von 2F + 1 Koordinatoren replizierte und ist beschlussfähig, solange „mindestens F + 1 von ihnen korrekt arbeiten". Ein Ausfall von bis zu F Koordinatoren blockiert niemanden mehr.

Und dann kommt der Satz, der das Verhältnis der beiden Welten klärt: „Der klassische Zwei-Phasen-Commit-Algorithmus ergibt sich als Spezialfall F = 0 des Paxos-Commit-Algorithmus."

Lies das zweimal. 2PC ist nicht ein anderes Protokoll als Konsens. 2PC ist Konsens – mit einer Replikatgruppe der Größe eins. Die Blockierungsanfälligkeit des klassischen 2PC ist nicht ein Designfehler, sondern genau der Preis dafür, dass man den Koordinator nicht repliziert hat.

Der Praxisbeweis: Spanner

Google hat genau dieses Rezept gebaut. Spanner (Corbett et al., OSDI 2012) führt verteilte Transaktionen über mehrere Shards aus, und zwar mit Zwei-Phasen-Commit über Paxos-Gruppen: Jeder Teilnehmer der 2PC-Transaktion ist nicht ein einzelner Server, sondern eine Paxos-replizierte Gruppe mit eigenem Leader. Der Koordinator ist ebenfalls eine solche Gruppe. Dadurch verschwindet das Blockierungsproblem praktisch: Ein ausgefallener Koordinator-Leader wird von seiner Gruppe in Sekunden ersetzt, und das 2PC-Log überlebt in den Replikaten.

Spanner zeigt damit, dass verteilte Transaktionen über Rechenzentren hinweg möglich sind. Was es gleichzeitig zeigt: Dafür braucht man eine Infrastruktur, die Atomuhren und GPS-Empfänger in jedes Rechenzentrum stellt, um Zeitunsicherheit explizit zu machen (siehe Uhren, die ihre eigene Unsicherheit kennen: Google Spanner, TrueTime und die Beherrschung der Zeit in der Cloud). Verteilte Atomarität ist eine Datenbank-Eigenschaft, die man kauft – nicht eine Architektur-Eigenschaft, die man über fremde Dienste drüberlegt.


Teil 5: Die Rechnung – was verteilte Atomarität wirklich kostet

Angenommen, du bekommst die Korrektheit hin. Was zahlst du?

Erstens: Latenz. Vier Nachrichtenverzögerungen plus mindestens zwei erzwungene Log-Flushes. Innerhalb eines Racks sind das Millisekunden, über Regionen hinweg Hunderte. Entscheidend ist aber nicht der Mittelwert, sondern das Maximum: Die Transaktion ist erst fertig, wenn der langsamste Teilnehmer geantwortet hat. Bei fünf Teilnehmern bestimmt effektiv das 99,99-Perzentil die gefühlte Antwortzeit – das ist genau der Mechanismus aus Der lange Schwanz der Latenz: Tail Latency und warum Mittelwerte in der Cloud lügen.

Zweitens: Sperrdauer. Alle Sperren werden über die gesamte Protokolldauer gehalten, inklusive Netzwerklatenz. Da der Durchsatz unter Konkurrenz invers mit der Sperrdauer skaliert, bedeutet eine Verzehnfachung der Transaktionsdauer unter Konkurrenz grob eine Zehntelung des Durchsatzes auf den heißen Datensätzen.

Drittens, und am wichtigsten: Verfügbarkeit multipliziert sich. Eine atomare Transaktion über n Dienste ist nur dann erfolgreich, wenn alle n verfügbar sind. Die Verfügbarkeiten multiplizieren sich:

Dienste in einer atomaren Transaktion Verfügbarkeit (je 99,9 %) Ausfallzeit pro 30-Tage-Monat
1 99,900 % ca. 43 min
2 99,800 % ca. 1 h 26 min
3 99,700 % ca. 2 h 10 min
5 99,501 % ca. 3 h 35 min
10 99,004 % ca. 7 h 10 min

Das ist die eigentliche Pointe. Die Kopplung ist nicht nur eine Performance-Frage – sie ist eine Verfügbarkeitsentscheidung. Wer fünf Dienste in eine atomare Transaktion zwingt, hat bewusst beschlossen, dass eine Buchung unmöglich ist, wenn irgendeiner von fünf Diensten gerade neu startet. Das ist bei einer Banküberweisung möglicherweise genau richtig. Bei einer Hotelreservierung ist es fast sicher falsch.


Teil 6: XA, MSDTC und die Praxis in .NET

Die Industrie hat verteilte Transaktionen natürlich standardisiert. X/Open veröffentlichte 1991 die XA-Spezifikation, die die Schnittstelle zwischen einem globalen Transaktionsmanager und einem Resource Manager beschreibt – also festlegt, was eine Datenbank oder ein Message Broker können muss, um an einem Zwei-Phasen-Commit teilzunehmen. Die Schnittstelle stammt aus dem Tuxedo-System der 1980er Jahre, und sie ist bis heute in vielen relationalen Datenbanken und Message-Brokern implementiert.

Im .NET-Ökosystem heißt das Werkzeug System.Transactions. Du schreibst:

using (var scope = new TransactionScope())
{
    using (var conn1 = new SqlConnection(dbA)) { conn1.Open(); /* ... */ }
    using (var conn2 = new SqlConnection(dbB)) { conn2.Open(); /* ... */ }
    scope.Complete();
}

Und das funktioniert tatsächlich. Microsofts Dokumentation beschreibt den Mechanismus dahinter sehr genau: Solange nur eine dauerhafte Ressource beteiligt ist, läuft es als „promotable transaction" – eine leichtgewichtige lokale SQL-Server-Transaktion ohne Koordinator-Overhead. Sobald eine zweite dauerhafte Ressource sich einschreibt, erfolgt die „dynamische Eskalation": System.Transactions zieht den Microsoft Distributed Transaction Coordinator (MSDTC) hinzu, und ab diesem Moment läuft ein echtes 2PC mit allem, was Teil 5 beschrieben hat.

Drei Details aus dieser Dokumentation lohnen das Merken, weil sie in Code-Reviews regelmäßig überraschen:

  1. TransactionScope erzeugt standardmäßig eine Transaktion mit Isolationsstufe Serializable – die strengste und sperrintensivste. Microsoft rät ausdrücklich dazu, das in Anwendungen mit Konkurrenz zu senken.
  2. Das Standard-Timeout eines TransactionScope ist eine Minute. Eine nicht committete oder nicht zurückgerollte Transaktion kann die Performance „erheblich verschlechtern" – weshalb das using-Block-Muster hier nicht Stil, sondern Pflicht ist.
  3. In SQL-Server-CLR-Kontexten erzwingt TransactionScope immer eine Eskalation, selbst wenn man es nur mit der Kontextverbindung benutzt.

Und eine Fußnote, die viel über den Status verteilter Transaktionen in der modernen Welt erzählt: Beim Sprung von .NET Framework auf .NET (Core) fiel die MSDTC-basierte verteilte Transaktion zunächst komplett weg. Sie wurde erst Jahre später nachgeliefert – und zwar ausschließlich für Windows (dotnet/runtime #715). Ein plattformübergreifender verteilter Transaktionsmanager ist bis heute ein offener Wunsch (#71769). Ich bin der Meinung, dass dies kein Versäumnis ist, sondern eine implizite Architekturaussage: Die Plattform, die auf Linux-Container und Cloud-Deployment ausgelegt wurde, hat die verteilte Transaktion nicht mitgenommen, weil die Zielarchitektur sie nicht mehr vorsieht.

Die hässliche Ecke: heuristische Entscheidungen

Ein letztes Detail, das man kennen sollte, bevor man XA produktiv einsetzt. Weil in-doubt-Zustände Sperren halten und Betriebsabläufe lahmlegen, bieten viele XA-Implementierungen einen Notausgang: Ein Administrator (oder ein Timeout) kann eine vorbereitete Transaktion einseitig committen oder abbrechen, ohne den Koordinator. Das nennt man eine heuristische Entscheidung. Sie löst das Betriebsproblem – und bricht dabei genau die Atomarität, für die man das ganze Protokoll gebaut hat. Ein heuristischer Abbruch auf einem Knoten, während der Koordinator committed hat, produziert eine dauerhafte, stille Inkonsistenz zwischen zwei Datenbanken, die beide überzeugt sind, korrekt gehandelt zu haben. Wer XA betreibt, braucht ein Verfahren für diesen Fall – und es ist kein technisches, sondern ein fachliches.


Teil 7: Der Gegenentwurf – Sagas

1987, also sechs Jahre nach Skeens 3PC, stellten Hector Garcia-Molina und Kenneth Salem auf der SIGMOD eine ganz andere Frage. Ihr Papier heißt schlicht „Sagas" (Proceedings of the 1987 ACM SIGMOD International Conference on Management of Data, Seiten 249–259). Es ging ihnen ursprünglich nicht um Microservices – die gab es nicht –, sondern um long-lived transactions (LLT): Transaktionen, die lange laufen, etwa ein Batch über Millionen Datensätze, und deren Sperren in dieser Zeit das ganze System ersticken.

Ihre Beobachtung ist von bestechender Einfachheit: Die sequenzielle Komposition von Transaktionen ist selbst keine Transaktion. Wenn man eine lange Transaktion in viele kurze zerlegt, bekommt man Durchsatz – und verliert Atomarität.

Ihre Antwort: Ersetze das Versprechen. Eine Saga ist eine Folge von Transaktionen T1, T2, …, Tn, von denen jede für sich sofort committed. Für jede Transaktion Ti existiert eine kompensierende Transaktion Ci, die deren Wirkung semantisch rückgängig macht. Scheitert T4, führt das System C3, C2, C1 in umgekehrter Reihenfolge aus. Das Ergebnis ist nicht der exakte Ausgangszustand, sondern – in der Formulierung des Papiers – eine akzeptable Annäherung an den Zustand vor Beginn der Saga. Die Garantie ist nicht mehr „alles oder nichts", sondern „beobachtbar äquivalent zu vollständig oder gar nicht".

Entscheidend ist das Wort semantisch. Eine Kompensation ist kein Rollback. Ein Rollback lässt keine Spur zurück; eine Kompensation ist ein neues Geschäftsereignis.

Aktion Ti Rollback (gibt es nicht) Kompensation Ci (das Richtige)
Hotelzimmer reserviert Reservierung nie passiert Reservierung storniert, ggf. Stornogebühr
Kreditkarte belastet Buchung nie passiert Gutschrift/Refund als eigene Buchung
Bestätigungs-E-Mail versandt E-Mail ungeschrieben Zweite E-Mail: „Ihre Buchung konnte nicht abgeschlossen werden"
Lagerbestand dekrementiert Bestand nie verändert Bestand inkrementiert, Reservierung freigegeben

Das dritte Beispiel ist das lehrreichste: Eine verschickte E-Mail kann man nicht zurückrollen. Man kann ihr nur eine zweite nachsenden. Das ist die Grenze jeder Kompensationsarchitektur – und der Grund, warum man in Sagas die unkompensierbaren Schritte ganz nach hinten legt (das „Pivot"-Prinzip: alles vor dem Pivot ist kompensierbar, alles danach ist wiederholbar bis zum Erfolg).

Die Saga braucht dafür einen dauerhaften Fortschrittsspeicher, das Saga-Log: Welcher Schritt wurde begonnen, welcher abgeschlossen, welche Kompensation steht aus. Fällt der Saga-Koordinator aus, liest der Nachfolger das Log und macht weiter – entweder vorwärts (forward recovery, bis zum Erfolg) oder rückwärts (backward recovery, bis zur vollständigen Kompensation). Dass dieses Log Zustand als Folge von Ereignissen statt als Momentaufnahme führt, verwandt mit Das Logbuch der Wahrheit: Event Sourcing und CQRS verstehen, ist kein Zufall: Beide Muster beruhen darauf, dass man einen Ablauf rekonstruieren können muss, um ihn fortsetzen zu können.

Was Sagas nicht geben: Isolation

Hier liegt der Fehler, den ich in Architekturdiskussionen am häufigsten sehe. Sagas ersetzen Atomarität durch Kompensation. Isolation ersetzen sie durch nichts.

Zwischen T1 und T4 ist der Zwischenzustand für jeden sichtbar. Das Hotelzimmer ist reserviert, der Flug noch nicht; wenn die Saga später scheitert und kompensiert, hat in der Zwischenzeit möglicherweise jemand auf dem reservierten Zustand Entscheidungen getroffen. Im Jargon: Sagas erlauben dirty reads und lost updates auf fachlicher Ebene. Das Phänomen heißt „kompensationsgefährdeter Lesevorgang", und es ist kein Bug, sondern das Wesen der Konstruktion.

Die Gegenmaßnahmen sind fachlich, nicht technisch:

  • Semantische Sperre: Der Datensatz bekommt einen Zwischenstatus („reserviert, nicht bestätigt"), den andere Leser respektieren müssen.
  • Kommutative Updates: Operationen so formulieren, dass die Reihenfolge nicht zählt – Betrag -= 50 statt Betrag = 950. Dieselbe Idee, die hinter Einigkeit ohne Abstimmung: CRDTs und die Kunst der konfliktfreien Replikation steht.
  • Pessimistische Reihenfolge: Den Schritt mit dem größten Kompensationsschaden so weit nach hinten schieben, dass er kaum noch kompensiert werden muss.
  • Wiederauslesen vor Ausführung: Vor dem kritischen Schritt prüfen, ob die Annahmen noch gelten.

Wer eine Saga baut und diese Liste nicht durchgearbeitet hat, hat keine Saga gebaut, sondern eine Folge von Datenbankaufrufen mit Hoffnung.


Teil 8: Helland und das Ende der Illusion

2007 schrieb Pat Helland – ein Mann, der bei Tandem, Microsoft und Amazon an genau diesen Systemen gearbeitet hatte – ein Positionspapier für die CIDR-Konferenz mit dem schönsten Untertitel der Fachliteratur: „Life beyond Distributed Transactions: an Apostate's Opinion". Ein Abtrünniger, der von seinem eigenen Glauben berichtet.

Seine Diagnose: Trotz jahrzehntelanger Investition in verteilte Transaktionsprotokolle meiden Entwickler in der Praxis diese Protokolle. Er vergleicht sie mit der Maginot-Linie – teuer gebaut, und dann umgangen. Wo Projekte es doch versuchen, „scheitern die Projekte, weil die Performance-Kosten und die Fragilität sie unpraktikabel machen".

Sein Gegenvorschlag besteht aus zwei Begriffen, die man in jeder modernen Microservice- oder Aktoren-Architektur wiederfindet:

Eine Entity ist eine Datensammlung mit eindeutigem Schlüssel, die vollständig in einen Serialisierbarkeitsbereich passt – also in eine Partition, auf eine Maschine. Und dann der Satz, der die ganze Architektur bestimmt: „Atomare Transaktionen können Entities nicht überspannen. Der Programmierer muss sich immer an die Daten innerhalb einer einzigen Entity halten."

Weil atomare Updates Entity-Grenzen nicht überschreiten können, wird asynchrones Messaging zum Mechanismus, um Arbeit über Entity-Grenzen hinweg zu koordinieren. Nachrichten werden an Entity-Schlüssel adressiert, nicht an physische Orte – dadurch kann das System jederzeit neu partitionieren, ohne dass die Geschäftslogik es merkt. Helland nennt diese Trennung scale-agnostic (die obere Schicht mit der Fachlogik) und scale-aware (die untere Schicht, die weiß, wo welche Entity liegt).

Der Preis dieser Freiheit ist eine Eigenschaft, die man explizit bauen muss: Weil Nachrichten bei Neupartitionierung mehrfach und in anderer Reihenfolge ankommen können, muss die Verarbeitung idempotent sein. Eine Activity ist in Hellands Modell der Zustand, den eine Entity pro Kommunikationspartner führt, um sich zu merken, welche Nachrichten sie schon verarbeitet hat.

Helland hat dieses Thema 2012 in „Idempotence Is Not a Medical Condition" (ACM Queue 10(4)) noch einmal vertieft. Die praktische Konsequenz für jede Saga-Implementierung:

  • Idempotenzschlüssel: Jede Nachricht trägt eine eindeutige ID; der Empfänger speichert verarbeitete IDs in derselben lokalen Transaktion wie den fachlichen Effekt und verwirft Duplikate.
  • Transactional Outbox: Du kannst nicht atomar „in die Datenbank schreiben und eine Nachricht senden" – das wäre schon wieder eine verteilte Transaktion. Also schreibst du den fachlichen Effekt und die zu sendende Nachricht in einer lokalen Transaktion in dieselbe Datenbank und lässt einen separaten Prozess die Outbox-Tabelle abarbeiten. Das Ergebnis ist at-least-once-Zustellung – weshalb Punkt 1 nicht optional ist.
  • Keine nackten Timestamps als Entscheidungsgrundlage: Ob eine Nachricht „neu" ist, entscheidet kausale Ordnung, nicht Wanduhrzeit (siehe Die Ordnung ohne Uhr: Lamport-Zeitstempel, Vektoruhren und die Kausalität verteilter Systeme).

Die Kombination „lokale Transaktion + Outbox + Idempotenzschlüssel + Kompensation" ist, nüchtern betrachtet, der tatsächliche Stand der Technik für Geschäftsprozesse über Dienstgrenzen. Sie ist mehr Arbeit als TransactionScope. Sie ist dafür auch dann verfügbar, wenn ein Dienst gerade neu startet.


Teil 9: Die Gegenbewegung – 2PC ist nicht tot

Es wäre zu einfach, hier zu enden. Die letzten zehn Jahre Forschung haben nämlich gezeigt, dass ein großer Teil der 2PC-Kritik nicht dem Protokoll gilt, sondern seiner Umgebung: einem einzelnen, nicht replizierten Koordinator, Sperren über Festplatten-I/O-Latenzen, und Netzwerken aus den 1980er Jahren.

Das deutlichste Beispiel ist Chardonnay (Tamer Eldeeb, Xincheng Xie, Philip A. Bernstein, Asaf Cidon, Junfeng Yang, OSDI 2023). Das System benutzt klassisches, sperrbasiertes 2PC – und macht es schnell, mit zwei Ideen:

Der Dry Run. Vor der eigentlichen Ausführung läuft die Transaktion einmal „trocken" mit sperrfreien Snapshot-Lesevorgängen durch. Dabei lernt das System, welche Datensätze gebraucht werden, und lädt sie in den Speicher. Erst dann läuft die echte Transaktion – und hält ihre Sperren nur noch für Arbeit, die vollständig im RAM passiert. Die Platten-I/O wandert aus dem Konkurrenzfenster heraus.

Der Epoch Service. Ein replizierter, monoton steigender Zähler dient als Serialisierungspunkt; entscheidend ist, dass „die Epoche von jeder Transaktion nur gelesen, nicht erhöht wird" – damit ist er kein Durchsatz-Flaschenhals.

Die Zahlen sind beeindruckend. Mit schnellem RPC (eRPC) und NVMe für das Write-Ahead-Log landet die 2PC-Latenz bei etwa 150 Mikrosekunden. Und unter extremer Konkurrenz ist „Chardonnays Durchsatz nur 15 % niedriger als unter extrem niedriger Konkurrenz", wo ein klassisches System-R*-artiges System um über 85 % einbricht. Auf TPC-C-New-Order skaliert das System linear auf über 200 Knoten bei stabilen 3–4 ms 2PC-Latenz im 99. Perzentil.

Der Kontrast zu Teil 5 ist lehrreich. Innerhalb eines Rechenzentrums, mit replizierten Koordinatoren, RDMA-artigem Netzwerk und NVMe-Logs ist 2PC ein hervorragendes Protokoll. Über Organisationsgrenzen hinweg, zwischen Diensten, die unterschiedliche Teams in unterschiedlichen Clouds betreiben und unabhängig voneinander deployen, ist es eine schlechte Idee – nicht, weil die Mathematik anders wäre, sondern weil die Annahmen anders sind.

Ich bin der Meinung, dass dies die eigentliche Lektion ist: Die Frage „2PC oder Saga?" ist falsch gestellt. Die richtige Frage lautet: Liegt die Grenze, über die ich eine Konsistenzgarantie spannen will, innerhalb oder außerhalb einer gemeinsam betriebenen Vertrauens- und Latenzdomäne?


Teil 10: Ein Entscheidungsrahmen

Kriterium Verteilte Transaktion (2PC/XA) Saga mit Kompensation
Atomarität Ja (echte Unteilbarkeit) Nein, nur „beobachtbar äquivalent"
Isolation Ja (meist Serializable) Nein – explizite Gegenmaßnahmen nötig
Verfügbarkeit Produkt aller Teilnehmer Jeder Dienst unabhängig verfügbar
Latenz 4 Nachrichtenverzögerungen + Log-Flushes Summe der Schritte, aber ohne globale Sperren
Kopplung Hoch, auch im Betrieb (Deploy, Version) Niedrig, nur Vertragskopplung
Betriebsrisiko In-doubt-Zustände, heuristische Entscheidungen Hängende Sagas, fehlerhafte Kompensationen
Komplexität liegt in der Infrastruktur in der Fachlogik
Passt, wenn eine Datenbank/Plattform, ein Team, ein RZ, harte Invarianten (Geld) Dienstgrenzen, Fremdsysteme, lange Laufzeit, Verfügbarkeit vor Momentankonsistenz

Drei Heuristiken, die mir in der Praxis am meisten geholfen haben:

  1. Verschiebe die Grenze, bevor du sie überspannst. Die beste verteilte Transaktion ist die, die nicht nötig ist, weil die beiden Datensätze in derselben Entity liegen. Ein Aggregat-Zuschnitt, der eine Invariante einschließt, erspart dir ein ganzes Protokoll. Prüfe diese Option immer zuerst.
  2. Frage, wer die Inkonsistenz bezahlt. Bei Geldbewegungen und regulierten Buchungen ist die Antwort oft „ein Wirtschaftsprüfer", und dann ist Atomarität ihren Preis wert – möglichst innerhalb einer Datenbank. Bei Reservierungen, Katalogen, Benachrichtigungen ist die Antwort „niemand, wenn wir innerhalb von Sekunden kompensieren".
  3. Schreibe die Kompensation zuerst. Wenn du Ci nicht formulieren kannst, hast du den Schritt Ti nicht verstanden. Und wenn Ci unmöglich ist, muss Ti nach hinten – hinter den Pivot.

Erkenntnis zum Mitnehmen

Verteilte Transaktionen sind nicht an mangelnder Theorie gescheitert. Die Theorie ist vollständig: 2PC ist korrekt, es ist der Spezialfall F = 0 von Paxos Commit, und mit replizierten Koordinatoren und schnellen Netzen ist es heute wieder ein ausgezeichnetes Protokoll. Was sie scheitern lässt, ist eine Eigenschaft, die man nicht wegoptimieren kann: Atomarität über n unabhängig betriebene Systeme multipliziert deren Ausfallwahrscheinlichkeiten und hält Sperren über Netzwerklatenzen. Das ist keine Implementierungsfrage, sondern Arithmetik.

Die praktische Konsequenz ist unbequem, aber befreiend: Wenn du Atomarität nicht bezahlen kannst, dann versprich sie nicht – schreibe das Versprechen um. Eine Saga, die ehrlich sagt „jeder Schritt committed sofort, und für jeden gibt es eine Kompensation, und zwischendurch ist der Zustand sichtbar, deshalb hat der Datensatz einen Zwischenstatus", ist eine korrekte Architektur. Eine Kette von Service-Aufrufen, die implizit behauptet, atomar zu sein, ist eine Fehlerquelle mit Verzögerungszünder.

Konkret, wenn du diese Woche eine Sache tun willst: Nimm einen Geschäftsprozess in deinem System, der über zwei oder mehr Dienste läuft, und schreibe für jeden Schritt die kompensierende Aktion auf – als fachlichen Satz, nicht als Code. Dann markiere den Pivot: den ersten Schritt, der nicht mehr kompensierbar ist. Wenn dieser Pivot nicht am Ende liegt, hast du gerade den wichtigsten Befund deines Quartals gefunden.


Querverweise im Vault

Das Unmöglichkeitsresultat von Skeen und Stonebraker gehört in dieselbe Familie wie FLP, und Paxos Commit macht die Verwandtschaft explizit: Wie Maschinen sich einig werden: Verteilter Konsens von FLP über Paxos zu Raft. Wie Google verteilte Transaktionen über Kontinente trotzdem praktikabel macht, hängt unmittelbar an der explizit gemachten Zeitunsicherheit: Uhren, die ihre eigene Unsicherheit kennen: Google Spanner, TrueTime und die Beherrschung der Zeit in der Cloud. Das Saga-Log ist ein naher Verwandter des Ereignisprotokolls: Das Logbuch der Wahrheit: Event Sourcing und CQRS verstehen. Die Gegenmaßnahme „kommutative Updates" ist dieselbe Mathematik, die Repliken ohne Absprache konvergieren lässt: Einigkeit ohne Abstimmung: CRDTs und die Kunst der konfliktfreien Replikation. Warum man Duplikate nicht an der Wanduhr erkennt, steht hier: Die Ordnung ohne Uhr: Lamport-Zeitstempel, Vektoruhren und die Kausalität verteilter Systeme. Und warum bei fünf Teilnehmern der Mittelwert der Antwortzeit nichts aussagt: Der lange Schwanz der Latenz: Tail Latency und warum Mittelwerte in der Cloud lügen.


Zum Nachdenken

Die Sagas-Arbeit ersetzt „alles oder nichts" durch „eine akzeptable Annäherung an den Zustand vor Beginn". Das ist eine fachliche, keine technische Festlegung – jemand muss entscheiden, was akzeptabel heißt.

Welche Konsistenzversprechen gibt dein System heute implizit, ohne dass irgendwo aufgeschrieben steht, wer sie erteilt hat? Und wenn du für den wichtigsten dieser Prozesse die Kompensation jedes Schrittes formulieren müsstest – an welcher Stelle würdest du merken, dass du nicht weißt, was passieren soll, wenn es schiefgeht?


Quellen

← All articles