PostgreSQL-Transaktions-Isolationsstufen in der Praxis
PostgreSQL implementiert drei verschiedene Isolationsstufen — Read Committed, Repeatable Read und Serializable. (READ UNCOMMITTED existiert zur Kompatibilität mit dem SQL-Standard, verhält sich aber genau wie Read Committed: PostgreSQL zeigt Ihnen niemals uncommittete Daten.) Die Unterschiede zählen nur unter Nebenläufigkeit, also genau dann, wenn sie am schwersten zu debuggen sind — es lohnt sich also, sie vor dem Zwischenfall zu kennen.
Read Committed: die Voreinstellung und ihr blinder Fleck
Jede Anweisung sieht einen Snapshot der Datenbank zum Zeitpunkt des Beginns dieser Anweisung. Zwei Konsequenzen:
- Zwei identische
SELECTs innerhalb einer Transaktion können unterschiedliche Ergebnisse liefern, wenn jemand dazwischen committet (non-repeatable read). - Das klassische Lost Update: zwei Transaktionen lesen beide einen Kontostand von 100, berechnen beide 100 − 10, schreiben beide 90. Eine Abhebung verschwindet. Read Committed verhindert dieses Muster nicht, wenn das Lesen und das Schreiben getrennte Anweisungen sind.
Die idiomatischen Abhilfen auf dieser Stufe:
-- make the read-modify-write atomic in one statement:
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
-- or lock the row at read time:
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
Ein nützliches Detail: wenn ein UPDATE unter Read Committed eine Zeile vorfindet, die bereits von einer nebenläufigen committeten Transaktion geändert wurde, wertet es seine WHERE-Klausel auf der neuen Version neu aus und fährt fort. Sie erhalten keinen Fehler — was bequem und gelegentlich die Quelle sehr subtiler Bugs ist.
Repeatable Read: ein Snapshot für die gesamte Transaktion
Der Snapshot wird bei der ersten Abfrage genommen und bis zum Commit beibehalten: jede Anweisung sieht dieselben Daten. In PostgreSQL verhindert dies auch Phantom Reads (stärker, als der SQL-Standard verlangt). Der Preis zeigt sich bei Schreibvorgängen:
ERROR: could not serialize access due to concurrent update
Wenn Sie versuchen, eine Zeile zu ändern, die eine nebenläufige Transaktion nach der Aufnahme Ihres Snapshots geändert und committet hat, kann PostgreSQL nicht so tun, als wären beide Reihenfolgen geschehen — es bricht Sie ab. Ihre Anwendung muss darauf vorbereitet sein, die gesamte Transaktion erneut zu versuchen. Repeatable Read ist die natürliche Stufe für konsistente Mehrfach-Abfrage-Lesevorgänge: Berichte, Exporte, Backups (pg_dump läuft auf dieser Stufe).
Serializable: als liefen Transaktionen nacheinander
Serializable in PostgreSQL ist Repeatable Read plus SSI (Serializable Snapshot Isolation): die Engine verfolgt Lese-/Schreib-Abhängigkeiten zwischen nebenläufigen Transaktionen und bricht eine ab, sobald ein Zyklus das Ergebnis unter keiner seriellen Reihenfolge möglich machen würde. Sie fängt Anomalien ab, die Repeatable Read nicht kann — der Lehrbuchfall ist Write Skew:
-- Two doctors, rule: at least one must stay on call.
-- T1: SELECT count(*) FROM oncall; -- sees 2, so it's safe to leave
-- T2: SELECT count(*) FROM oncall; -- sees 2, so it's safe to leave
-- T1: DELETE FROM oncall WHERE doctor = 'alice'; COMMIT;
-- T2: DELETE FROM oncall WHERE doctor = 'bob'; COMMIT;
-- Repeatable Read: both commit, zero doctors on call.
-- Serializable: one of them gets error 40001 and must retry.
Kosten: etwas CPU und Speicher für das Abhängigkeits-Tracking und — wichtig — False Positives: Transaktionen können „vorsichtshalber" abgebrochen werden. Alle Transaktionen der Last müssen Serializable laufen, damit ihre Garantien gelten, und jede einzelne von ihnen braucht Retry-Logik.
Korrekt erneut versuchen
- Versuchen Sie bei SQLSTATE
40001(serialization_failure) und40P01(deadlock_detected) erneut. - Versuchen Sie die gesamte Transaktion erneut, einschließlich ihrer Lesevorgänge — die Werte, die Sie beim ersten Mal gelesen haben, sind genau das, was möglicherweise nicht mehr zutrifft. Nur die fehlgeschlagene Anweisung erneut auszuführen ist falsch.
- Halten Sie erneut versuchte Transaktionen kurz und begrenzt (begrenzte Versuche, kleiner Backoff). Eine Transaktion, die die halbe Datenbank liest, wird für immer weiterkollidieren.
- Kapseln Sie keine Seiteneffekte (E-Mails, HTTP-Aufrufe) in eine Transaktion, die Sie erneut zu versuchen beabsichtigen.
Wahl in der Praxis
| Situation | Vernünftige Wahl |
|---|---|
| Typisches OLTP mit gezielten Zeilen-Updates | Read Committed + atomare Updates / SELECT FOR UPDATE, wo es darauf ankommt |
| Mehrfach-Anweisungs-Lesevorgänge, die untereinander konsistent sein müssen | Repeatable Read |
| Invarianten über mehrere Zeilen („mindestens eine", „Summe darf nicht überschreiten", Doppelbuchung) | Serializable mit Retries — oder explizites Sperren, wenn die Hotspots wenige und bekannt sind |
| Job-Queues | Read Committed + FOR UPDATE SKIP LOCKED |
Setzen Sie die Stufe pro Transaktion (BEGIN ISOLATION LEVEL SERIALIZABLE) oder pro Session; Stufen zu mischen ist in Ordnung, außer dass, wie erwähnt, die Garantien von Serializable erfordern, dass alle zusammengehörigen Transaktionen sie verwenden.
