PostgreSQL implementiert Serializable Snapshot Isolation (SSI) unter Verwendung von Prädikatsverriegelung und Serialisierungsgraphentest, um echte Serialisierbarkeit zu erreichen, ohne die Leistungsnachteile der herkömmlichen Zwei-Phasen-Verriegelung. Der 40001-Fehler (serialization_failure) tritt speziell während write skew oder read-write conflicts auf, bei denen zwei Transaktionen einen rw-abhängigkeit-Zyklus bilden. Zum Beispiel, Transaktion A liest Zeilen, die ein Prädikat erfüllen (z.B. WHERE color = 'red'), Transaktion B liest Zeilen, die ein nicht überlappendes Prädikat erfüllen (z.B. WHERE color = 'blue'), dann aktualisiert A Zeilen auf 'blue', während B Zeilen auf 'red' aktualisiert. Keine Transaktion blockiert die andere, aber das Ergebnis ist nicht serialisierbar.
Dieses Muster stellt eine gefährliche Struktur im Serialisierungsgraphen dar: zwei aufeinanderfolgende rw-antidependencies, die einen potenziellen Zyklus bilden. PostgreSQL erkennt dies und bricht eine Transaktion ab, um anomale Zustände zu verhindern. Das Problem ist subtil, da die Transaktionen unterschiedliche physische Zeilen ändern könnten, wodurch der Konflikt unsichtbar für die Zeilenverriegelungsmechanismen wird, die in niedrigeren Isolationsniveaus verwendet werden.
Die vorgeschriebene Lösung erfordert, dass die Anwendung eine optimistische Retry-Schleife implementiert. Beim Fangen der SQL EXCEPTION '40001' muss die Anwendung die aktuelle Transaktion zurückrollen und die gesamte Operation mit exponentiellem Backoff erneut versuchen. Im Gegensatz zu Deadlocks, die typischerweise durch sofortige Wiederholung gelöst werden, profitieren Serialisierungsfehler unter hoher Belastung von jittered delays, um stampfende Herden zu verhindern.
-- Beispiel für Anwendungs-Retry-Logik in PL/pgSQL DO $$ DECLARE retries INT := 0; max_retries INT := 3; BEGIN WHILE retries < max_retries LOOP BEGIN SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE; PERFORM * FROM inventory WHERE category = 'electronics' AND count > 0; UPDATE inventory SET count = count - 1 WHERE item_id = 123; COMMIT; EXIT; EXCEPTION WHEN SQLSTATE '40001' THEN ROLLBACK; retries := retries + 1; PERFORM pg_sleep(power(2, retries) * 0.1); -- Exponentielles Backoff END; END LOOP; END $$;
Eine Ticketbörse für Konzerte ermöglichte es den Nutzern, Sitzkategorien über Check-then-Act-Logik zu tauschen. Transaktion A überprüfte, ob VIP-Sitze verfügbar waren, und downgradete dann einen gehaltenen VIP-Sitz auf Standard. Zeitgleich bestätigte Transaktion B die Verfügbarkeit der Standard und hob einen Standard-Sitz auf VIP. Unter READ COMMITTED lasen beide Transaktionen die Verfügbarkeit als wahr, führten Updates durch, und das System endete mit negativen Beständen in beiden Kategorien, obwohl jede Transaktion die Einschränkungen überprüfte.
Drei Lösungen wurden entworfen. Die erste verwendete explizite SELECT FOR UPDATE-Sperrung, aber dies versagte, als die Verfügbarkeitsabfragen null Zeilen zurückgaben, wodurch keine Sperren erlangt wurden und das System anfällig für phantome Inserts blieb. Der zweite Ansatz implementierte ADVISORY LOCKS unter Verwendung von pg_try_advisory_lock(), um den Zugriff auf Sitzkategorien zu serialisieren, was Konflikte verhinderte, aber komplexe Sperrordnungsrisiken einführte und den Durchsatz um 40% reduzierte aufgrund der Serialisierung aller Kategoriekontrollen.
Die dritte Lösung nahm die SERIALIZABLE-Isolation mit einer Anwendungsebene-Retry-Schleife an. Dies wurde gewählt, da es die Korrektheit ohne manuelle Sperrverwaltung garantierte, und die Retry-Überhängungen waren akzeptabel gegeben der niedrigen Frequenz gleichzeitiger Tauschvorgänge im Verhältnis zu Leseoperationen. Die Implementierung verwendete einen JDBC-Retry-Handler, der SQLException mit SQLState 40001 abfing, wartete 100ms * 2^versuch und führte die Transaktion erneut aus. Dies eliminierte Vorbuchungsvorfälle vollständig, obwohl die p99-Latenz während der Hauptverkaufszeiten um 15 ms anstieg.
Was ist der genaue Unterschied zwischen Prädikatsverriegelungen in der serialisierbaren Isolation und Zeilenverriegelungen in der wiederholbaren Lesezeit?
Wiederholbares Lesen verhindert nicht-wiederholbare Lesevorgänge, indem es die tatsächlich von einer Abfrage zurückgegebenen Zeilen sperrt, verhindert jedoch nicht phantome Lesevorgänge - neue Zeilen, die von anderen Transaktionen eingefügt werden, die die WHERE-Klausel der Abfrage erfüllen würden. Serialisierbare Isolation verwendet Prädikatsverriegelungen, die den Suchbereich selbst sperren und das Einfügen verhindern, das das Abfrageprädikat erfüllt, selbst in Zeilen, die nicht existierten, als die Abfrage ausgeführt wurde. Kandidaten verwechseln dies häufig und glauben fälschlicherweise, dass Wiederholtes Lesen phantome Lesevorgänge verhindert oder dass Serialisierbar nur vorhandene Zeilen sperrt.
Wie bestimmt der Algorithmus zum Testen des Serialisierungsgraphen, welche Transaktion abzubrechen ist, wenn ein Zyklus erkannt wird?
PostgreSQL verwendet eine Strategie, bei der der "erste Kommittent gewinnt“ kombiniert mit der Erkennung gefährlicher Strukturen. Wenn ein rw-Konflikt (Lese-Schreib-Abhängigkeit) zwischen konkurrierenden Transaktionen entsteht, verfolgt das System, ob diese Kante einen Zyklus im Serialisierungsgraphen vervollständigt. Die Transaktion, die den Zyklus vervollständigt, wird mit SQLSTATE 40001 abgebrochen. Die Wahl ist deterministisch basierend auf der Struktur des Graphen und nicht aufs Alter der Transaktion, wobei die Abbruchkosten der Transaktionen, deren Rückrollung am wenigsten kostspielig oder zuletzt im erkannten Zyklus ist, begünstigt wird. Zu verstehen, dass dies eine präventive Abbruch ist (um eine ungültige Geschichte zu verhindern) und nicht ein Deadlock (Warten auf Sperren), ist entscheidend für die richtige Fehlerbehandlung.
Warum könnte SELECT FOR UPDATE versagen, um Serialisierungsfehler in Szenarien zu verhindern, in denen die serialisierbare Isolation einen Konflikt erkennt?
SELECT FOR UPDATE erwirbt ROW SHARE-Sperren nur auf Zeilen, die zum Zeitpunkt der Ausführung existieren. In Check-then-Act-Mustern, in denen die anfängliche Abfrage null Zeilen zurückgibt (z.B. die Verfügbarkeit von null Sitzen überprüfen), erwirbt FOR UPDATE überhaupt keine Sperren, was es einer anderen Transaktion erlaubt, die konfliktierende Zeile einzufügen. Serialisierbare Isolation erkennt dies als Prädikatskonflikt, da das "null Zeilen"-Ergebnis eine gültige Lesegruppe darstellt, die durch das gleichzeitige Einfügen ungültig gemacht wurde. Kandidaten nehmen oft fälschlicherweise an, dass FOR UPDATE umfassenden Schutz bietet, ohne zu erkennen, dass es keinen Schutz gegen phantome Inserts bietet, wenn das Prädikat zu Beginn mit nichts übereinstimmt.