Zum Inhalt springen

Zwei-Phasen-Commit (2PC) erklärt: Prepare, Commit und warum er blockiert

Wie der Zwei-Phasen-Commit Transaktionen über Datenbanken atomar macht, was bei einem Koordinator-Ausfall passiert und wann Sagas oder eine Outbox besser passen.

· Gründer & Entwickler

Veröffentlicht am

Sequenzdiagramm mit Anwendung, Koordinator, Bestell- und Zahlungsdatenbank. Ein par-Fragment zeigt PREPARE an beide Datenbanken und YES-Stimmen zurück. Ein alt-Fragment zeigt COMMIT und Bestätigungen, wenn alle mit Ja stimmen, und ROLLBACK bei einem Nein.

Eine Bestellung wird in eine Datenbank geschrieben, die Zahlung in eine andere. Beides muss passieren oder keines von beiden. Jede Datenbank kann ihren eigenen Teil atomar committen, aber nichts Lokales verhindert, dass die eine committet, während die andere abstürzt. Der Zwei-Phasen-Commit (2PC) ist das klassische Protokoll für dieses Problem, über den XA-Standard in die meisten relationalen Datenbanken eingebaut und heute auch innerhalb verteilter SQL-Datenbanken im Einsatz.

Er hat auch eine berühmte Schwäche: Fällt der Koordinator im falschen Moment aus, können Teilnehmer mit gehaltenen Sperren festhängen, ohne selbst entscheiden zu können. Dieser Beitrag geht das Protokoll mit einem Sequenzdiagramm durch, dazu jeden Fehlerfall, wie echte Systeme es umsetzen und wann eine Saga oder eine Outbox das bessere Werkzeug ist.

Die Rollen

  • Koordinator oder Transaktionsmanager: steuert das Protokoll und entscheidet über das Ergebnis. Er kann eine Bibliothek in deinem Anwendungsserver sein, ein eigener Service oder eine Komponente einer verteilten Datenbank.
  • Teilnehmer oder Ressourcenmanager: die Datenbanken oder Queues, die jeweils einen Teil der Transaktion halten.

Jede Partei führt ein dauerhaftes Log. Die Log-Schreibvorgänge sind kein Implementierungsdetail; sie sorgen dafür, dass das Protokoll Abstürze übersteht.

Das Protokoll

Phase 1: Prepare.

  1. Die Anwendung erledigt ihre Arbeit in jedem Teilnehmer innerhalb einer globalen Transaktion und bittet dann den Koordinator, zu committen.
  2. Der Koordinator schickt PREPARE an jeden Teilnehmer.
  3. Jeder Teilnehmer stellt sicher, dass er committen kann, egal was als Nächstes passiert: Er schreibt die Änderungen der Transaktion und einen Prepare-Eintrag in sein dauerhaftes Log, behält seine Sperren und stimmt mit YES. Kann er nicht committen, etwa weil ein Constraint verletzt wurde, stimmt er mit NO und rollt zurück.

Phase 2: Commit oder Abort.

  1. Sind alle Stimmen YES, schreibt der Koordinator eine COMMIT-Entscheidung in sein eigenes Log. Dieser Schreibvorgang ist der Commit-Punkt: Ab hier ist die Transaktion committed, selbst wenn eine Millisekunde später jede Maschine abstürzt.
  2. Der Koordinator schickt COMMIT an jeden Teilnehmer. Jeder committet, gibt seine Sperren frei und bestätigt.
  3. Stimmt ein Teilnehmer mit NO oder antwortet er nicht rechtzeitig, entscheidet der Koordinator auf ABORT und schickt stattdessen ROLLBACK.

Eine YES-Stimme ist ein Versprechen. Nach einer YES-Stimme hat ein Teilnehmer das Recht aufgegeben, allein zu entscheiden: Er muss committen, wenn er dazu aufgefordert wird, auch nach einem Neustart, und er kann nicht von sich aus zurückrollen, weil der Koordinator sich vielleicht schon für den Commit entschieden hat.

Die Kosten

Bei N Teilnehmern braucht ein erfolgreicher Commit zwei Round Trips vom Koordinator und vier Nachrichten pro Teilnehmer: PREPARE, Stimme, COMMIT und Bestätigung. Jeder Teilnehmer schreibt sein Log zweimal erzwungen auf die Platte, beim Prepare und beim Commit, und der Koordinator erzwingt seine Entscheidung einmal. Sperren werden die ganze Zeit gehalten, also entscheidet der langsamste Teilnehmer, wie lange jeder andere Teilnehmer konkurrierende Arbeit blockiert.

Was bei Fehlern passiert

Fehler Wann Ergebnis
Teilnehmer stürzt ab oder läuft in einen Timeout Vor der Abstimmung Der Koordinator bricht die Transaktion ab
Teilnehmer stimmt mit NO Phase 1 Der Koordinator bricht die Transaktion ab
Teilnehmer stürzt ab Nach einer YES-Stimme Beim Neustart findet er den Prepare-Eintrag und fragt den Koordinator nach dem Ergebnis; bis dahin hält er seine Sperren
Koordinator stürzt ab Bevor er die Entscheidung schreibt Vorbereitete Teilnehmer warten. Bei der Wiederherstellung bricht der Koordinator ab, weil keine Entscheidung in seinem Log steht
Koordinator stürzt ab Nachdem er COMMIT geschrieben hat Bei der Wiederherstellung liest er sein Log und schickt COMMIT erneut
Netzwerkpartition Nachdem ein Teilnehmer mit YES gestimmt hat Dieser Teilnehmer blockiert, bis er den Koordinator erreicht

Entscheidend sind die Zeilen, in denen ein Teilnehmer mit YES gestimmt hat und die Entscheidung nicht hört. Er kann nicht committen, weil ein anderer Teilnehmer mit NO gestimmt haben könnte. Er kann nicht abbrechen, weil der Koordinator committet haben könnte. Er wartet mit gehaltenen Sperren, solange der Koordinator nicht erreichbar ist. Jede andere Transaktion, die diese Zeilen berührt, wartet ebenfalls. Genau das ist gemeint, wenn man 2PC ein blockierendes Protokoll nennt.

Hängende vorbereitete Transaktionen in der Praxis

PostgreSQL setzt die Teilnehmerseite mit PREPARE TRANSACTION, COMMIT PREPARED und ROLLBACK PREPARED um. Die Funktion ist standardmäßig deaktiviert (max_prepared_transactions ist 0), und die Dokumentation warnt, dass sie für externe Transaktionsmanager gedacht ist, nicht für Anwendungen. Eine vergessene vorbereitete Transaktion behält ihre Sperren und hindert VACUUM daran, Speicher zurückzugewinnen.

Eine solche zu finden und aufzulösen sieht so aus:

-- Prepared transactions waiting for a decision
SELECT gid, prepared, owner, database
FROM pg_prepared_xacts
ORDER BY prepared;

-- Only after checking the coordinator's log for the real outcome:
COMMIT PREPARED 'order-7f3a';
-- or
ROLLBACK PREPARED 'order-7f3a';

Löse eine vorbereitete Transaktion nie durch Raten auf. Hat der Koordinator committet und du rollst einen Teilnehmer zurück, hast du genau die Inkonsistenz geschaffen, die 2PC verhindern soll. MySQL bietet denselben Mechanismus mit XA PREPARE, XA COMMIT und XA RECOVER.

Varianten, die den Schmerz lindern

  • Presumed Abort. Hat der Koordinator keinen Eintrag zu einer Transaktion, lautet die Antwort Abort. Das spart Log-Schreibvorgänge für abgebrochene Transaktionen und vereinfacht die Wiederherstellung; die meisten Implementierungen nutzen es.
  • Read-only-Teilnehmer. Ein Teilnehmer, der nichts geändert hat, kann mit „read-only“ stimmen und Phase 2 überspringen.
  • Drei-Phasen-Commit. Fügt eine Pre-Commit-Runde hinzu, damit Teilnehmer ohne den Koordinator abschließen können, aber nur, wenn Nachrichtenverzögerungen begrenzt sind und das Netzwerk nie partitioniert. Diese Annahmen gelten selten, deshalb ist 3PC vor allem ein Lehrbuchthema.
  • Koordinator und Teilnehmer replizieren. Die echte Lösung gegen Blockieren ist, jede Rolle zu einer replizierten Gruppe zu machen, die den Verlust einer Maschine übersteht. Googles Spanner betreibt aus genau diesem Grund Zwei-Phasen-Commit über Paxos-Gruppen, und andere verteilte SQL-Datenbanken nutzen dieselbe Idee mit Raft. Der Zwei-Phasen-Commit entscheidet weiterhin über das Ergebnis; Konsens hält jede Partei verfügbar.

Wann 2PC passt und wann nicht

2PC passt, wenn alle Teilnehmer Datenbanken oder Broker sind, die einen Prepare-Schritt unterstützen, du alle selbst kontrollierst, Transaktionen kurz sind und du Atomarität und Isolation über sie hinweg brauchst. Schlecht funktioniert er über Services anderer Teams hinweg, über HTTP-APIs ohne Prepare-Schritt (ein Zahlungsanbieter hält deine Abbuchung nicht in einem vorbereiteten Zustand) und bei Geschäftsprozessen, die Minuten oder Tage dauern.

Zwei-Phasen-Commit Saga Transactional Outbox
Konsistenz Atomar; Sperren sorgen für Isolation Eventual, mit kompensierenden Schritten Eventual zwischen einer Datenbank und einem Broker
Blockieren Ja, solange eine Entscheidung aussteht Nein Nein
Voraussetzung Teilnehmer, die Prepare unterstützen Kompensationslogik für jeden Schritt Eine Datenbank plus ein Relay
Typischer Einsatz Datenbanken oder Shards, die du betreibst Geschäftsabläufe über Services hinweg Events nach einem Schreibvorgang zuverlässig veröffentlichen

Das Saga-Pattern-Diagramm zeigt die kompensierende Alternative, und das Transactional-Outbox-Pattern behandelt den häufigsten Fall, in dem Teams zu 2PC greifen: in eine Datenbank schreiben und gemeinsam eine Nachricht veröffentlichen. Oft ist die beste Lösung strukturell: Leg die Daten, die sich gemeinsam ändern müssen, in eine Datenbank oder einen Shard, wie das Datenbank-Sharding-Diagramm erläutert.

Das Zwei-Phasen-Commit-Diagramm

Sequenzdiagramm mit Anwendung, Koordinator, Bestell- und Zahlungsdatenbank. Ein par-Fragment zeigt PREPARE an beide Datenbanken und YES-Stimmen zurück. Ein alt-Fragment zeigt COMMIT und Bestätigungen, wenn alle mit Ja stimmen, und ROLLBACK bei einem Nein.

10 Komponenten · 12 Verbindungen

In Archboard öffnen
Diagramminhalt
  • Application
  • Coordinator
  • Orders DB
  • Payments DB
  • phase 1: prepare
  • phase 2: all votes YES
  • commit()
  • PREPARE
  • YES (prepare record on disk)
  • COMMIT
  • ACK, locks released
  • ACK
  • ROLLBACK
  • committed or aborted
Zwei-Phasen-Commit über zwei Datenbanken. In Phase 1 bittet der Koordinator beide Teilnehmer um Prepare und sammelt ihre Stimmen; in Phase 2 protokolliert er die Entscheidung und weist alle an zu committen oder, falls eine Stimme Nein war, zurückzurollen.

So liest du es:

  • Vier Lebenslinien. Die Anwendung, der Koordinator, die Bestelldatenbank und die Zahlungsdatenbank. Die Zeit läuft nach unten.
  • Das par-Fragment ist Phase 1. PREPARE geht gleichzeitig an beide Teilnehmer, und jeder gibt eine Stimme zurück, nachdem er seinen Prepare-Eintrag geschrieben hat.
  • Das alt-Fragment ist Phase 2. Der obere Zweig ist der Normalfall: Alle Stimmen sind YES, der Koordinator protokolliert COMMIT, und beide Teilnehmer committen und bestätigen. Der untere Zweig ist der Abbruchpfad.
  • Die Notiz markiert das In-doubt-Fenster zwischen einer YES-Stimme und der Entscheidung, in dem Teilnehmer Sperren halten und nicht allein entscheiden können.
  • Der Markdown-Block listet die Fehlerfälle aus der Tabelle oben auf.

Verteilte Transaktionen ehrlich zeichnen

  • Zeig die Log-Schreibvorgänge. Ein 2PC-Diagramm ohne „Prepare-Eintrag schreiben“ und „Entscheidung schreiben“ lässt genau den Teil weg, der es korrekt macht.
  • Markiere den Commit-Punkt. Eine Nachricht oder ein Log-Schreibvorgang entscheidet über das Ergebnis; zeig darauf.
  • Zeichne das Fenster, in dem es blockiert. Es ist der Grund, warum Teams 2PC meiden; ein Diagramm, das es versteckt, führt in die Irre.
  • Leg Fehlerpfade in eigene Diagramme. Ein Koordinator-Absturz nach dem Prepare verdient eine eigene Sequenz mit den Wiederherstellungsschritten.

Zur Notation selbst behandelt das Sequenzdiagramm-Beispiel Lebenslinien, Fragmente und Benennung.

In Archboard nachbauen

  1. Melde dich mit Google oder GitHub an und leg ein Diagramm an.
  2. Füge vier Lebenslinie-Nodes aus dem Sequenz-Pack hinzu: Application, Coordinator, Orders DB und Payments DB.
  3. Zeichne die Nachrichten als Kanten von oben nach unten: Commit-Anfrage, PREPARE an jede Datenbank, die YES-Stimmen, COMMIT an jede Datenbank und die Bestätigungen.
  4. Füge ein Kombiniertes Fragment aus dem Sequenz-Pack mit dem Operator par um die Prepare-Nachrichten hinzu und ein zweites Kombiniertes Fragment mit dem Operator alt um die Commit- und Rollback-Zweige.
  5. Füge eine Notiz für das In-doubt-Fenster hinzu und einen Markdown-Block aus dem Basis-Pack mit der Fehlertabelle.

Das Beispiel nutzt 7 Diagramm-Nodes und bleibt damit im Free-Plan.

FAQ

Warum nennt man den Zwei-Phasen-Commit ein blockierendes Protokoll? Ein Teilnehmer, der mit YES gestimmt hat, kann weder allein committen noch abbrechen. Wird der Koordinator unerreichbar, bevor er die Entscheidung schickt, wartet der Teilnehmer mit gehaltenen Sperren, und mit ihm jede Transaktion, die diese Zeilen braucht.

Ist der Zwei-Phasen-Commit dasselbe wie eine verteilte Transaktion? Er ist der gängigste Weg, eine zu committen. Eine verteilte Transaktion erstreckt sich über mehrere Ressourcen; der Zwei-Phasen-Commit ist das Protokoll, das ihren Commit atomar macht.

Löst der Drei-Phasen-Commit das Blockierproblem? Nur unter Annahmen, die selten gelten: begrenzte Nachrichtenverzögerungen und keine Netzwerkpartitionen. Die praktische Lösung ist, Koordinator und Teilnehmer mit Konsens zu replizieren, wie Spanner es tut.

Kann ich den Zwei-Phasen-Commit zwischen Microservices nutzen? Selten gut. Die Services müssten einen Prepare-Schritt anbieten und füreinander Sperren halten, was ihre Verfügbarkeit koppelt. Zwischen Services sind Sagas und das Outbox-Pattern die übliche Wahl.

Garantiert der Zwei-Phasen-Commit Isolation? Er garantiert Atomarität. Isolation kommt von den Sperren der Teilnehmer, die 2PC hält, bis die Entscheidung eintrifft, sodass nebenläufige Transaktionen keine Teilergebnisse sehen.

Kostenlos starten — ein Seat, keine Kreditkarte.

Ähnliche Beiträge

← Zurück zum Blog

In einer Minute ein Canvas öffnen

Melden Sie sich mit Google oder GitHub an. Kostenlos für einen Sitzplatz. Upgraden Sie, wenn das Team dazukommt.