In Teil 1 dieser Reihe ging es um strukturelle Fehler: fehlende Fremdschlüssel, falsche Indizes, Soft Deletes die keine sind. Am Ende stand ein Versprechen: Transaktionen. Ein Thema das in der Theorie einfach klingt und in der Praxis erschreckend oft falsch eingesetzt wird — oder gar nicht.
Dabei ist das Konzept nicht kompliziert. Eine Transaktion fasst mehrere Datenbankoperationen zu einer einzigen atomaren Einheit zusammen. Entweder alles gelingt — oder nichts wird geschrieben. Kein Zwischenzustand, kein halbfertiger Datensatz, keine inkonsistente Datenbank.
Das klingt nach einem Sicherheitsnetz für Ausnahmefälle. Es ist in Wirklichkeit das Fundament für jeden Schreibvorgang der aus mehr als einem SQL-Statement besteht.
Was passiert ohne Transaktion
Ein klassisches Beispiel: Eine Bestellung wird angelegt. Die Bestellung selbst wird geschrieben, dann die Bestellpositionen, dann wird der Lagerbestand aktualisiert. Drei Operationen. Kein COMMIT, kein BEGIN.
-- Ohne Transaktion: drei unabhängige Operationen
INSERT INTO orders (user_id, total) VALUES (42, 149.90);
INSERT INTO order_items (order_id, product_id, qty) VALUES (LAST_INSERT_ID(), 7, 2);
UPDATE products SET stock = stock - 2 WHERE id = 7;
Wenn nach dem ersten INSERT die Verbindung abbricht — durch einen Timeout, einen Serverabsturz, einen Netzwerkfehler — existiert eine Order ohne Items. Der Lagerbestand wurde noch nicht angefasst. Der Nutzer hat bezahlt, die Bestellung ist halb da, und niemand weiß es.
Das klingt nach einem überschaubaren Problem — bis man es zu Ende denkt. Stell dir einen Shop mit 300 Artikeln vor, die alle 99 Euro kosten. In der Datenbank steht jetzt: Kunde hat etwas für 99 Euro bestellt. Welches Produkt? Keine Bestellposition, kein Hinweis. Was macht man jetzt? Den Kunden fragen — peinlich. Alle 300 Artikel schicken — existenzbedrohend. Warten bis der Kunde sich meldet — unprofessionell. Die Bestellung ignorieren — noch schlimmer. Es gibt keine gute Antwort, weil die Datenbank in einem Zustand ist der nie hätte eintreten dürfen.
Das ist kein hypothetisches Szenario. Das passiert. Nicht täglich, aber wenn es passiert ist der Schaden da — und er ist schwer zu finden, weil keine Fehlermeldung auftaucht. Die Datenbank hat ja funktioniert. Nur nicht vollständig.
Die Lösung: BEGIN, COMMIT, ROLLBACK
BEGIN;
INSERT INTO orders (user_id, total) VALUES (42, 149.90);
INSERT INTO order_items (order_id, product_id, qty) VALUES (LAST_INSERT_ID(), 7, 2);
UPDATE products SET stock = stock - 2 WHERE id = 7;
COMMIT;
Mit BEGIN markiert man den Start einer Transaktion. Mit COMMIT werden alle Änderungen gleichzeitig und dauerhaft geschrieben. Schlägt irgendetwas dazwischen fehl, rollt die Datenbank alles zurück — automatisch. Die Datenbank bleibt in dem Zustand in dem sie vor dem BEGIN war.
Wichtig: Das Rollback kommt nicht aus PHP. MySQL erledigt das selbst — auch dann wenn der Server einfach abstürzt, der Strom ausfällt oder die Verbindung mitten in der Transaktion verloren geht. Kein Anwendungscode muss dafür aktiv eingreifen. Die Datenbank-Engine stellt beim Neustart sicher, dass keine halb geschriebene Transaktion im System verbleibt. Das ist kein Komfort-Feature — das ist ein Grundprinzip der Datenbankarchitektur. Ein Rollback kann natürlich auch „manuell“ aus PHP kommen — etwa wenn per Code nachträglich festgestellt wird, das etwas nicht so ist, wie es sein sollte. Das kann beispielsweise dann der Fall sein, wenn es keine direkte Fehlerquelle gibt, aber z.B. der Count von einem INSERT nicht alle erwarteten Zeilen umfasst.
In PHP mit PDO sieht das so aus:
try {
->beginTransaction();
= ->prepare('INSERT INTO orders (user_id, total) VALUES (?, ?)');
->execute([42, 149.90]);
= ->lastInsertId();
= ->prepare('INSERT INTO order_items (order_id, product_id, qty) VALUES (?, ?, ?)');
->execute([, 7, 2]);
= ->prepare('UPDATE products SET stock = stock - ? WHERE id = ?');
->execute([2, 7]);
->commit();
} catch (Exception ) {
->rollBack();
// Logging, Fehlerbehandlung
throw ;
}
Entweder alle drei Statements gelingen, oder keines wird geschrieben. Keine halbfertigen Datensätze. Keine manuelle Aufräumaktion. Die Datenbank kümmert sich darum. Wichtig dabei: ROLLBACK macht im Fehlerfall die Datenbank selbst. COMMIT muss aber IMMER manuell gesetzt werden. Ausnahme wäre nur, wenn die Datenbank auf „autocommit“ läuft. Das ist aber eher der schlechteste Zustand, weil dieser genauso wirkt wie ganz ohne Transaktion. Und somit ggf. Dinge commited, die gar nicht zum COMMIT vorgesehen sind. Denn, und auch das ist wichtig zu verstehen: Ein erfolgtes COMMIT kann nicht via ROLLBACK zurückgenommen werden. Transaktionen sind also nicht beliebig oft/lange wieder zurückzunehmen.
Wann sind Transaktionen zwingend nötig
Die kurze Antwort: immer wenn mehrere Schreiboperationen logisch zusammengehören und eine davon ohne die andere keinen Sinn ergibt. Das gilt übrigens auch für einzelne Statements — etwa wenn in der Datenbank eigene Stored Functions oder Procedures hinterlegt sind, die intern selbst schreiben. Wichtig dabei: Diese Funktionen dürfen nicht als autonome Transaktion laufen oder ein eigenes COMMIT auslösen, sonst kollidieren sie mit der äußeren Transaktion. Beispiele dafür, wann Transaktionen immer sinnvoll sind:
- Zahlungen und Buchungen. Ein Guthaben wird abgezogen, gleichzeitig wird eine Buchung angelegt. Fällt eine der beiden Operationen aus, ist entweder Geld weg ohne Buchung — oder eine Buchung existiert ohne Abbuchung. Beides ist falsch.
- Benutzererstellung mit Rollen. Ein neuer Nutzer wird angelegt und bekommt sofort eine Standardrolle zugewiesen. Gelingt die Rollenzuweisung nicht, existiert ein Nutzer ohne Berechtigungen — möglicherweise mit Zugang der keiner definierten Rolle entspricht.
- Lagerbestand und Reservierung. Wird eine Reservierung angelegt aber der Lagerbestand nicht angepasst, kann dasselbe Produkt mehrfach reserviert werden. Klassischer Overselling-Bug — und fast immer das Ergebnis einer fehlenden Transaktion.
- Datenmigration. Wer Daten in einem laufenden System transformiert und umschreibt sollte das grundsätzlich in einer Transaktion tun. Bricht die Migration ab, bleibt die Datenbank im alten Zustand. Ohne Transaktion hat man eine Datenbank halb im alten, halb im neuen Zustand — und muss manuell herausfinden was schon migriert wurde und was nicht.
Isolation: Was andere Verbindungen sehen dürfen
Transaktionen lösen nicht nur das Problem des Halbschreibens. Sie lösen auch das Problem des gleichzeitigen Lesens und Schreibens. Sprich man kann sie ebenfalls dazu „benutzen“ um dafür zu sorgen, dass nicht zwei Zugriffe gleichzeitig auf den gleichen Datensatz erfolgen.
Angenommen, zwei Nutzer kaufen gleichzeitig das letzte Stück eines Produkts. Ohne Isolation lesen beide einen Lagerbestand von 1, beide legen eine Bestellung an, beide reduzieren den Bestand auf 0 — und der Lagerbestand liegt plötzlich bei -1.
-- Szenario: Lagerbestand = 1, zwei gleichzeitige Anfragen
-- Verbindung A liest: stock = 1 → legt Bestellung an
-- Verbindung B liest: stock = 1 → legt Bestellung an
-- Verbindung A schreibt: stock = stock - 1 → stock = 0
-- Verbindung B schreibt: stock = stock - 1 → stock = -1
Die Lösung ist eine pessimistische Sperre mit SELECT … FOR UPDATE. Die Zeile wird beim Lesen gesperrt, bis die Transaktion abgeschlossen ist. Keine andere Verbindung kann sie in der Zwischenzeit verändern.
BEGIN;
SELECT stock FROM products WHERE id = 7 FOR UPDATE;
-- Zeile ist jetzt gesperrt. Andere Verbindungen warten.
UPDATE products SET stock = stock - 1 WHERE id = 7 AND stock > 0;
-- Nur wenn stock > 0 wird die Bestellung fortgesetzt
COMMIT;
Das ist keine Premature Optimization. Das ist der korrekte Umgang mit konkurrierenden Schreibzugriffen — besonders in E-Commerce, Buchungssystemen und allem was begrenzte Ressourcen verwaltet. Wichtig dabei zu wissen: Einige Frameworks und Programme öffnen selbst eine Transaktion. Nutzt man dann in seinem eigenen Plugin oder Erweiterungscode eine eigene neue Transaktion, kann es passieren, dass sich beide Transaktionen verhaken und gegenseitig aufeinander warten. Das kann dann dazu führen, dass am Ende doch nichts geschrieben wird. Es empfiehlt sich also jeweils zu prüfen, ob und welche Transaktionen vielleicht schon per se geöffnet werden.
Was Transaktionen nicht lösen
DDL-Statements. In MySQL werden CREATE TABLE, ALTER TABLE und DROP TABLE automatisch committed — unabhängig davon ob man sich in einer Transaktion befindet. Wer eine Migration mit einer Transaktion absichern will muss in PostgreSQL arbeiten, das DDL-Rollbacks unterstützt.
Externe Seiteneffekte. Eine Transaktion rollt Datenbankänderungen zurück — aber keine E-Mails die schon raus sind, keine API-Calls die schon passiert sind, keine Webhooks die schon gefeuert haben. Wobei das alles Dinge sind, die grundsätzlich sowieso nicht aus einer Datenbank heraus passieren oder angestoßen werden sollten! Aber man muss es trotzdem mit bedenken und seinen Code entsprechend gestalten: Erst die Dinge in der Datenbank erledigen und erst wenn das auf COMMIT läuft kommen Webhooks, API-Calls und E-Mails dran.
Lange Transaktionen. Je länger eine Transaktion offen ist, desto mehr Locks werden gehalten. In Hochlastsystemen können lange Transaktionen zu Deadlocks führen. Transaktionen sollen so kurz wie möglich sein.
Fazit
Transaktionen sind kein optionales Feature für kritische Systeme. Sie sind der Standard für jeden Schreibvorgang der aus mehreren zusammenhängenden Operationen besteht. Wer sie weglässt baut auf einem Fundament das bei Last, Fehlern oder Timing-Problemen nachgibt — und zwar genau dann wenn es am ungünstigsten ist.
Der Einstieg ist gering: BEGIN, COMMIT, ROLLBACK. Die Auswirkung ist hoch: konsistente Daten, kein halbfertiger Zustand, kein manueller Aufwand bei Fehlern.
Teil 3 dieser Reihe widmet sich Datenbanksicherheit: Warum root ohne Passwort keine Option ist, warum sensible Spalteninhalte verschlüsselt gehören — und warum ein unverschlüsseltes Backup die gesamte Sicherheit einer Anwendung aushebelt.
Muss ich Transaktionen in Laravel/WordPress extra aktivieren?
Laravel umschließt Eloquent-Operationen nicht automatisch in Transaktionen; man muss DB::transaction() explizit nutzen. WordPress hat keine native Transaktions-API, PDO aber schon – PDO wird aber nicht überall konsequent genutzt in WordPress.
Was passiert wenn ich vergesse, COMMIT zu setzen?
Die Transaktion bleibt offen bis die Verbindung getrennt wird — dann rollt MySQL automatisch zurück. Nichts wird geschrieben. Heißt aber auch: Vor einem COMMIT sieht auch keine andere Datenbankabfrage die neuen Daten.
Sind Transaktionen langsamer?
Minimal, aber vernachlässigbar. Der Overhead ist kleiner als das Risiko inkonsistenter Daten.
Funktionieren Transaktionen mit allen MySQL-Tabellen?
Nur mit InnoDB. MyISAM unterstützt keine Transaktionen wer noch MyISAM nutzt, sollte migrieren.
Was ist der Unterschied zwischen ROLLBACK und einem Fehler ignorieren?
ROLLBACK macht alle Änderungen seit BEGIN rückgängig. Einen Fehler ignorieren lässt den halbfertigen Zustand in der Datenbank stehen. Fehler ignorieren bei INSERTS bedeutet auch, der Block bricht nicht ab, nur weil das INSERT fehlschlägt. Das kann innerhalb einer Transaktion dann leider doch zu halben bzw. inkonsistenten Daten führen und sollte bei Verwendung einer Transaktion nie und sonst nur sehr bedacht verwendet werden.

Kommentar verfassen