PHP Magazin

PHP, JavaScript, Open Web Technologies
X

Unser neues Quickvote: Welche PHP-Version nutzt ihr?

Wie XA-Transaktionen und das Zwei-Phasen-Commit-Protokoll aus ihnen Freunde machen

MySQL und Oracle

Ulf Wendel

Beim Versuch, dem endlosen Winter mit einem Billigflieger zu entfliehen, brach die Aggressivität hervor, die angeblich die Frühjahrsmüdigkeit begleitet. Warum sind der Kauf eines Flugs und die Hotelreservierung zwei getrennte Geschäftstransaktionen? Mit den XA-Transaktionen und dem Zwei-Phasen-Commit-Protokoll ist es technisch kein Problem, die zwei Datenbanken der Fluggesellschaft und einer Hotelvermittlung in einer globalen Transaktion anzusprechen. Die globale Transaktion stellt sicher, dass ein garantiertes Paket aus Flug und Hotel angeboten werden kann.

Das Zwei-Phasen-Commit-Protokoll (2PC) definiert ein Protokoll, bei dem ein Koordinator und mehrere Datenquellen innerhalb einer globalen Transaktion angesprochen werden können. Teilnehmen an globalen Transaktionen können alle Datenquellen, die das Protokoll unterstützen. Unterstützt wird das Protokoll vom MySQL Server seit der Version 5.0. Oracle, und IBMs DB2 können ebenso eingebunden werden wie viele andere Datenquellen, zum Beispiel Applikationen, wenn diese das Protokoll implementieren. Das Protokoll ist im Dokument „Distributed Transaction Processing: The XA Specification“ der X/Open Group [1] spezifiziert.
Egal, ob nun die Flugesellschaft eine Oracle-Datenbank und die Hotelvermittlung eine MySQL-Datenbank einsetzt, mit dem 2PC-Protokoll könnten beide Datenbanken friedlich miteinander vereint an einer globalen Transaktion teilnehmen. Oracle reserviert einen Flug, MySQL sucht das passende Hotel, eine Java-Anwendung bucht die Zugverbindung, und ich freue mich, dass ich mit einem Bezahlvorgang die Flucht in den Frühling antreten kann. Es gibt noch viel mehr Anwendungsbeispiele für verteilte Transaktionen als dieses egoistische, wenngleich reale Beispiel.

Das Zwei-Phasen-Commit-Protokoll (2PC) für verteilte Transaktionen

Das 2PC-Protokoll beschreibt, wie ein Koordinator (Transaction Manager) und mindestens ein weiterer Teilnehmer (Resource Manager) eine globale Transaktion durchführen und dabei ACID-Konformität erzielen können. Wie am Namen des Protokolls erkenntlich ist, besteht es aus zwei Phasen: der Wahlphase und der Entscheidungsphase.
Das Protokoll beginnt mit der Wahlphase. Der Koordinator befragt alle Teilnehmer, ob sie die ihnen angewiesene lokale Transaktion erfolgreich durchführen könnten. Die Entscheidung eines Teilnehmers, ob ein COMMIT ausgeführt werden könnte, kann nicht mehr vom Teilnehmer revidiert werden. Der Teilnehmer muss vor einer positiven Antwort alle notwendigen Vorkehrungen getroffen haben, damit ein Commit selbst nach einem Systemausfall durchgeführt werden könnte, wenn der Koordinator die entsprechende Anweisung gibt. Zur Erreichung dieser Forderung schreiben die meisten Teilnehmer Logeinträge auf ein dauerhaftes Medium, z.B. eine Festplatte, bevor das „Ja“ an den Koordinator gesendet wird.
Nachdem alle Teilnehmer ihre Antworten an den Koordinator geliefert haben, leitet dieser die zweite und abschließende Entscheidungsphase des 2PC-Protokolls ein. Anhand der erhaltenen Antworten entscheidet der Koordinator, ob die Teilnehmer ihre lokalen Transaktionen mit einem COMMIT abschließen sollen. Im einfachsten Fall bejahen alle Teilnehmer die Möglichkeit der Durchführung, und der Koordinator weist die Systeme an, das anschließende COMMIT durchzuführen.
Verneint ein Teilnehmer die Möglichkeit des COMMIT, dann muss der Koordinator entscheiden, ob die negative Antwort z.B. zu einer unerwünschten Verletzung der Atomarität der globalen Transaktion führen würde.

Die komplexe Rolle des Koordinators und offene Fragen

Der Koordinator agiert als Kontrollinstanz, die überwachen kann, ob ACID-Konformität erreicht wurde. Diese Überprüfung ist nicht Teil des 2PC-Protokolls. Es ist eine zusätzliche Aufgabe, die vom Koordinator wahrgenommen werden kann. Im Zweifelsfall muss die Applikation, die den Koordinator mit der Durchführung des 2PC-Protokolls beauftragt hat, die Richtlinien vorgeben, nach denen der Koordinator über COMMIT und ROLLBACK entscheidet.
Auf der Ebene der Applikation und des Koodinators ergeben sich viele Fragen: Wer überprüft die Einhaltung von zusätzlichen globalen Integritätsbedingungen und stellt die globale Konsistenz sicher? Was passiert, wenn der Koordinator ausfällt? Wie soll das System reagieren, wenn ein Teilnehmer ausfällt?
Die Überprüfung von globalen Integritätsregeln und die Reaktion auf den Ausfall eines Teilnehmers ist je nach Bedarf zu implementieren. Schwieriger zu beantworten ist die Frage, was beim Ausfall des Koordinators passieren soll und wie einem Ausfall vorgebeugt werden könnte.
In der vorgestellten Centralized-2PC-(C2PC-)genannten Variante des 2PC-Protokolls ist der zentrale Koordinator ein Single-Point-of-Failure. Ein Ausfall des Koordinators hinterlässt das Gesamtsystem in einen undefinierten Zustand. Vielleicht wurde deshalb in der XA-Spezifikation eine Hintertür eingebaut, die autonome Entscheidungen der Teilnehmer erlaubt. Wie später noch gezeigt wird, wurde auch ein Kommando definiert, dass die Fehlerbehandlung nach einem Neustart des Koordinators vereinfacht.
Doch nicht nur die Implementierung einer Fehlerbehandlung, sondern auch Abwandlungen des C2PC-Protokolls können die Situation verbessern. Beim linearen 2PC-Protokoll ist der Koordinator nur noch Initiator. Das verteilte 2PC-Protokoll beseitigt den Single-Point-of-Failure, führt aber zu erheblich mehr Kommunikationsaufwand. Auf der Ebene des Protokolls kann jedoch erst das Drei-Phasen-Commit-Protokoll [3] das Problem lösen.

Nicht die Kompexität eines Transaktionsmanagers unterschätzen

Wenn im Folgenden die seit MySQL 5.0 verfügbaren SQL-Anweisungen des 2PC-Protokolls [4] vorgestellt werden, dann sollten Sie immer im Hinterkopf behalten, dass die Anweisungen nur die Spitze des Eisbergs sind. Die Beherrschung der Kommandos ist trivial.
Die Konzeption eines Transaktionsmanagers - Koordinator und Applikation - ist jedoch eine der komplexesten Aufgaben, die es bei der Implementierung einer Datenbank zu bewältigen gibt. Während sich im Enterprise-Umfeld eigenständige Transaktionsmanager finden und Java-Anwender z.B. im MySQL/J Connector von einem Teil der Komplexität abgeschirmt werden, scheint es im PHP-Umfeld keine Prototypen und Implementierungsbeispiele zu geben.
Andererseits sollten Sie nicht gleich den Kopf in den Sand stecken: Überall dort, wo zwei Systeme zusammenarbeiten sollen, ermöglicht das 2PC-Protokoll neue, aufregende Möglichkeiten. Wenn Sie einfach nur zwei Systeme miteinander verbinden wollen und Sie im Fehlerfall kurzerhand einen Abbruch der globalen Transaktion durchführen, ist der Implementierungsaufwand gering und der Nutzen gewaltig.
Die anschließenden Beispiele beweisen, wie einfach die SQL-Anweisungen für XA-Transaktionen sind. Es wird klar, dass eine PHP-Anwendung, die sich bewusst auf eine einfache Implementierung des Koordinators (Transaktionsmanagers) beschränkt, das Zwei-Phasen-Protokoll und die XA-Transaktionen ohne viel Aufwand nutzen kann.



Abb. 1: Ablaufdiagramm einer einfachen XA-Transaktion

Zur Vorbereitung der Beispiele für XA-Anweisungen legen Sie eine einfache Tabelle namens xa_table mit einer einzigen Spalte ID vom Typ Integer an, die eine Speicher-Engine benutzt, welche das 2PC-Protokoll unterstützt. Unterstützt wird das Protokoll von InnoDB und NDB (MySQL-Cluster), wobei es im Falle von NDB nur intern zum Einsatz kommt.

# mysql -u ulf -p ulf
mysql>> CREATE TABLE xa_table(id INTEGER) ENGINE = InnoDB;

Die XA-Spezifikation ist eine Interface-Definition, die gleichzeitig das gesamte Modell einer globalen Transaktion beschreibt. Damit leicht ein Zusammenhang zwischen der Spezifikation des Interface und den SQL-Anweisungen hergestellt werden kann, werden im Artikel neben den SQL-Anweisungen auch die entsprechenden
xa_*

-Funktionen benannt. Diese Funktionen stehen dem PHP-Programmierer nicht zur Verfügung. Als PHP-Programmierer ist man darauf angewiesen, die SQL-Anweisungen zu verwenden, so wie es die Beispiele zeigen. Die Nennung der Funktionen dient ausschließlich dazu, den Bezug zur Spezifikation herzustellen, um dem Leser die Arbeit mit dem Referenzmaterial zu vereinfachen.

Transaction Branches - Zweige in einer globalen Transaktion

Gemäß der XA-Spezifikation ist eine globale Transaktion in voneinander unabhängige Zweige (Transaction Branches) unterteilt. Eine globale Transaktion beinhaltet einen oder mehrere Zweige. Im Artikel werden die Zweige fortan als lokale XA-Transaktion auf einem Teilnehmer (Resource Manager) bezeichnet. Auf einem Teilnehmer können gleichzeitig mehrere lokale XA-Transaktionen ablaufen, die zu einer globalen Transaktion gehören.
Eine globale Transaktion wird eingeleitet durch den Koordinator (Transaction Manager), der diese auf Anforderung einer Applikation hin startet. Der Koordinator baut eine Verbindung zu den Teilnehmern auf (Resource Manager) und sendet ein
xa_open()

-Kommando. Der Standard macht keine genauen Vorgaben darüber, wie der Teilnehmer auf ein
xa_open()

zu reagieren hat, und so fasst MySQL das Kommando mit dem Verbindungsaufbau zusammen. Das Gegenstück
xa_close()

entspricht dem Verbindungsabbau zu MySQL.

# mysql -u ulf -p ulf
mysql>>

Nachdem die Verbindung geöffnet wurde, startet der Koordinator eine lokale XA-Transaktion. Das ist eigentlich genau so, als würden Sie Autocommit deaktivieren und mit START TRANSACTION eine „normale“ Transaktion starten. Wenn Sie mit XA-Transaktionen arbeiten, stehen Ihnen jedoch nicht START TRANSACTION, COMMIT und ROLLBACK zur Verfügung, sondern etwas anders benannte Pendants der Anweisungen. Da Sie sich außerhalb einer „normalen“ Transaktion bewegen, verliert auch die Autocommit-Einstellung ihre Gültigkeit.

XA {START|BEGIN} xid [JOIN|RESUME]

XA-Spezifikation:
xa_start()

. Leitet eine XA-Transaktion ein. JOIN und RESUME werden derzeit nicht unterstützt. Die xid kann dreiteilig ausfallen:
gtrid [, bqual [, formatID ]].

Tipp: damit es garantiert keine Probleme mit Charsets gibt, die
gtrid

und
btrib

in hexadezimaler Darstellung übergeben.

XA END xid [SUSPEND [FOR MIGRATE]]

XA-Spezifikation:
xa_end()

. Schließt eine XA-Transaktion ab, löst den „Thread of Control“. Derzeit kann eine „abgeschlossene“ Transaktion nicht um neue Kommandos erweitert werden (siehe Text).

XA PREPARE xid

XA-Spezifikation:
xa_prepare()

. Kommando zur Einleitung Wahlphase des 2PC-Protokolls. Die Datenbank gibt eine nicht-revidierbare Auskunft, ob ein COMMIT erfolgreich ausgeführt werden könnte.

XA COMMIT xid [ONE PHASE]

XA-Spezifikation:
xa_commit()

. Committed eine XA-Transaktion. One Phase ist eine Protokolloptimierung bei der Prepare und Commit in einem Schritt zusammengefasst werden. Dies ist oft dann möglich, wenn ein Teilnehmer nur Leseoperationen ausgeführt hat. Wird XA COMMIT xid ONE PHASE gesendet, kann das sonst zwingend notwendige XA PREPARE ausgelassen werden. Vorteil: eine Operation weniger Kommunikationsaufwand.

XA ROLLBACK xid

XA-Spezifikation:
xa_rollback()

. Rollback einer XA-Transaktion.

XA RECOVER

XA-Spezifikation:
xa_recover()

. Liefert eine Liste von vorbereiteten XA-Transaktionen. Hilfreich bei der Fehlerbehandlung (siehe Text).

Der Branch Identifier XID und der Thread of Control

Jede lokale XA-Transaktion erhält vom Koordinator eine eindeutige XID (Branch Identifier), die zur Identifizierung der Transaktion dient. Die XID kann aus bis zu drei Elementen bestehen, die durch Kommata voneinander getrennt sind. Beim aktuellen Grade des Supports von XA durch MySQL ist nur das erste Element von semantischer Bedeutung, die anderen Elemente können ignoriert werden.

mysql>> XA START 'xatest';

Nachdem der Koordinator die XA-Transaktion gestartet hat, sollte er die Kontrolle an die Applikation abgeben, damit diese wie gewohnt Anweisungen an die Teilnehmer (hier: die MySQL-Datenbank) schicken kann. Gemäß Spezifikation darf hierbei der „Thread of Control“ nicht gewechselt werden. Mit Thread ist ein Betriebssystemthread gemeint. Es muss jener Betriebssystemthread SQL-Anweisungen senden, der das
xa_start()

ausgeführt hat. Andere Threads dürfen die mit der XID identifizierte XA-Transaktion zu diesem Zeitpunkt nicht verändern.
Für den typischen PHP-Anwender oder jemanden, der auf der Konsole die Beispiele nacharbeitet, hat dies keine Bedeutung. Da Sie nur mit einem Betriebssystemthread und einer Verbindung arbeiten, müssen Sie den „Thread of Control“ nur als theoretisches Konstrukt im Hinterkopf behalten. Benutzen Sie einfach die bestehende Verbindung zum Server und wechseln Sie ihre Rolle von „Koordinator“ zu „Applikation“, um anschließend zwei Beispieldatensätze als symbolische Handlung der Applikation anzulegen.

mysql>> INSERT INTO xa_table(id) VALUES (1);
mysql>> INSERT INTO xa_table(id) VALUES (2);

Nachdem die Applikation ihre Anweisungen, die zur lokalen XA-Transaktion gehören, durchgeführt hat, übernimmt der Koordinator wieder das Ruder. Der Koordinator hebt die Assoziation des Threads mit der XA-Transaktion mittel
xa_end()

auf und schließt die Liste der Kommandos, die zur Transaktion gehören sollen.

# mysql -u ulf -p ulf
mysql>>

Die Spezifikation sieht vor, dass nach der Trennung des Threads von der XA-Transaktion ein anderer Thread (MySQL: eine andere Verbindung/Session) die XA-Transaktion wieder öffnen könnte, um die Liste von Kommandos zu ergänzen, die zur Transaktion gehören. Derzeit ist diese Option von MySQL nicht implementiert.

Einleitung der Wahlphase

Sobald Applikation und Koordinator ihre Transaktionen auf allen Teilnehmern beendet haben, beginnt die Wahlphase des 2PC-Protokoll. Der Koordinator sendet an alle Teilnehmer das Kommando
xa_prepare()

.
xa_prepare()

kann erst dann beantwortet werden, wenn alle Assoziationen von Threads mit einer Transaktion mittels
xa_end()

aufgehoben wurden. Dies ist in der aktuellen Implementierung immer dann der Fall, nachdem
xa_end()

das erste Mal für eine Transaktion aufgerufen wurde. Für Sie bedeutet dies, dass ein XA PREPARE fehlschlägt, wenn Sie noch kein XA END gesendet haben.

# mysql -u ulf -p ulf
mysql>>

Die Antwort eines Teilnehmers auf das
xa_prepare()


-

Kommando ist verbindlich. Egal, ob der Koordinator sich entscheidet, anschließend ein
xa_commit()

oder
xa_rollback()

zu senden, der Teilnehmer garantiert, dass er die Anweisung umsetzen kann.

Wie das Zwei-Phasen-Commit-Protokoll die Ausfallsicherheit erhöhen kann

Nach Erhalt aller Antworten wird die Entscheidungsphase des 2PC-Protokoll eingeleitet. Sollten die Fluggesellschaft und die Hotelvermittlung beide signalisiert haben, dass ein Flug und ein Hotelzimmer zur Verfügung stehen, dann sendet der Koordinator das COMMIT. Statt der „normalen“ SQL-Anweisung COMMIT wird die Anweisung XA COMMIT verwendet. Wie üblich wird dem Teilnehmer durch Angabe der XID explizit mitgeteilt, welche XA-Transaktion abgeschlossen werden soll.

# mysql -u ulf -p ulf
mysql>>

Erkennt der Koordinator Schwierigkeiten, dann können die einzelnen XA-Transaktionen mittels ROLLBACK rückgängig gemacht werden.

# mysql -u ulf -p ulf
mysql>>

Damit sind alle SQL-Kommandos bis auf eines beschrieben. Im Fehlerfall kann der Koordinator die Reaktion der Teilnehmer einer globalen Transaktion abfragen, indem er sich von den Teilnehmern eine Liste aller vorbereiteten XA-Transaktionen anzeigen lässt.

# mysql -u ulf -p ulf
mysql>>

Wenn Sie mit MySQL experimentieren, werden Sie merken, dass es der MySQL-Server nicht immer „so genau nimmt“ mit dem Warten auf die Anweisung des Koordinators, um zu erfahren, was mit einer vorbereiteten Transaktion passieren soll. Das Verhalten ist jedoch dokumentiert [5]. Bricht die Verbindung des Koordinators zum Server ab oder wird der Server heruntergefahren, dann wird die vorbereitete XA-Transaktion gelöscht (ROLLBACK). Kommt es zu einem ungewollten Absturz des MySQL-Servers, bleibt die vorbereitete XA-Transaktion erhalten und der MySQL-Server wartet auf die Anweisungen des Koordinators.
Es gibt noch weitere Details, in denen die XA-Spezifikation noch nicht vollständig umgesetzt wurde. So fehlen einige erweiterte Funktionen zur dynamischen Registrierung von Teilnehmern, die im XA-Standard beschrieben sind. Außerdem kann der MySQL-Server selbst nur als Teilnehmer (Resource Manager) an einer globalen Transaktion auftreten (external XA). Der Server ist nicht in der Lage, die Rolle des Koordinators zu übernehmen (internal XA). Die Gründe sind in der oben genannten Seite des Handbuchs dokumentiert. Aber mal ehrlich: Niemand braucht in der Praxis mehr als aktuell vorhanden ist! Wer will schon die Datenbank als Koordinator (Transaction Manager) einsetzen, wenn er auch eine auf die jeweilige Anwendung maßgeschneiderte Lösung selbst implementieren kann.
Es ist alles implementiert, was im Rahmen von J2EE benötigt wird, und Java-Anwender setzen bereits die neuen Möglichkeiten des MySQL Connector/J ein. Es ist alles verfügbar, was PHP noch nie ausgenutzt hat. Es spricht also nichts dagegen, den Sonnenhungrigen schon bei der Reisebuchung für den Kurzurlaub mit Komfort zu verwöhnen und endlich Bundles aus Flug und Hotel anzubieten ...
Ulf Wendel arbeitet bei MySQL als MaxDB Support Manager. Wenn er nicht gerade von einem Italienurlaub träumt, bemüht er sich, alte Kontakte zur deutschen PHP-Community zu pflegen, die ihm in der Vergangenheit geholfen hat, sein Geld mit der Erstellung von PHP- und MySQL-basierten Webanwendungen zu verdienen.

 

Kommentare

Ihr Kommentar zum Thema

Als Gast kommentieren:

Gastkommentare werden nach redaktioneller Prüfung freigegeben (bitte Policy beachten).