Sperrproblem mit gleichzeitigem DELETE / INSERT in PostgreSQL

35

Das ist ziemlich einfach, aber ich bin verblüfft darüber, was PG macht (v9.0). Wir beginnen mit einer einfachen Tabelle:

CREATE TABLE test (id INT PRIMARY KEY);

und ein paar Zeilen:

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);

Mit meinem bevorzugten JDBC-Abfragetool (ExecuteQuery) verbinde ich zwei Sitzungsfenster mit der Datenbank, in der sich diese Tabelle befindet. Beide sind transaktionell (dh Auto-Commit = false). Nennen wir sie S1 und S2.

Das gleiche Stück Code für jeden:

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;

Führen Sie dies nun in Zeitlupe aus und führen Sie jeweils eine in den Fenstern aus.

S1-1 runs (1 row deleted)
S2-1 runs (but is blocked since S1 has a write lock)
S1-2 runs (1 row inserted)
S1-3 runs, releasing the write lock
S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???
S2-2 runs, reports a unique key constraint violation

Jetzt funktioniert dies gut in SQLServer. Wenn S2 löscht, wird 1 gelöschte Zeile gemeldet. Und dann funktioniert der Einsatz von S2 einwandfrei.

Ich vermute, dass PostgreSQL den Index in der Tabelle sperrt, in der sich diese Zeile befindet, während SQLServer den tatsächlichen Schlüsselwert sperrt.

Habe ich recht? Kann das funktionieren?

DaveyBob
quelle

Antworten:

39

Mat und Erwin haben beide Recht, und ich füge nur eine weitere Antwort hinzu, um das, was sie gesagt haben, in einer Weise weiter zu erläutern, die nicht in einen Kommentar passt. Da ihre Antworten nicht alle zufrieden zu stellen scheinen und es einen Vorschlag gab, die PostgreSQL-Entwickler zu konsultieren, und ich bin einer, werde ich näher darauf eingehen.

Der wichtige Punkt hierbei ist, dass nach dem SQL-Standard innerhalb einer Transaktion, die auf der READ COMMITTEDTransaktionsisolationsstufe ausgeführt wird, die Einschränkung besteht, dass die Arbeit nicht festgeschriebener Transaktionen nicht sichtbar sein darf. Wann die Arbeit festgeschriebener Transaktionen sichtbar wird, hängt von der Implementierung ab. Sie weisen darauf hin, dass sich zwei Produkte dafür entschieden haben, dies umzusetzen. Keine der Implementierungen verstößt gegen die Anforderungen des Standards.

Folgendes passiert in PostgreSQL im Detail:

S1-1 läuft (1 Zeile gelöscht)

Die alte Zeile bleibt bestehen, da S1 möglicherweise immer noch zurückgesetzt wird, S1 jedoch die Zeile sperrt, sodass jede andere Sitzung, die versucht, die Zeile zu ändern, darauf wartet, ob S1 festgeschrieben oder zurückgesetzt wird. Alle Lesevorgänge der Tabelle können die alte Zeile weiterhin anzeigen, es sei denn, sie versuchen, sie mit SELECT FOR UPDATEoder zu sperren SELECT FOR SHARE.

S2-1 läuft (ist aber gesperrt, da S1 eine Schreibsperre hat)

S2 muss nun warten, um das Ergebnis von S1 zu sehen. Wenn S1 nicht festgeschrieben, sondern zurückgesetzt würde, würde S2 die Zeile löschen. Beachten Sie, dass, wenn S1 vor dem Rollback eine neue Version eingefügt hätte, die neue Version aus Sicht einer anderen Transaktion niemals vorhanden gewesen wäre und die alte Version aus Sicht einer anderen Transaktion nicht gelöscht worden wäre.

S1-2 läuft (1 Zeile eingefügt)

Diese Reihe ist unabhängig von der alten. Wenn es eine Aktualisierung der Zeile mit id = 1 gegeben hätte, wären die alte und die neue Version miteinander verknüpft, und S2 könnte die aktualisierte Version der Zeile löschen, wenn sie entsperrt würde. Dass eine neue Zeile zufällig dieselben Werte wie eine in der Vergangenheit vorhandene Zeile hat, macht sie nicht zu einer aktualisierten Version dieser Zeile.

S1-3 wird ausgeführt und gibt die Schreibsperre frei

Die Änderungen von S1 werden also beibehalten. Eine Reihe ist weg. Eine Zeile wurde hinzugefügt.

S2-1 läuft, jetzt wo es die Sperre bekommen kann. Es werden jedoch 0 Zeilen gelöscht. HUH ???

Intern passiert, dass es einen Zeiger von einer Version einer Zeile zur nächsten Version derselben Zeile gibt, wenn diese aktualisiert wird. Wenn die Zeile gelöscht wird, gibt es keine nächste Version. Wenn eine READ COMMITTEDTransaktion bei einem Schreibkonflikt aus einem Block erwacht, folgt sie dieser Aktualisierungskette bis zum Ende. Wenn die Zeile nicht gelöscht wurde und die Auswahlkriterien der Abfrage noch erfüllt, wird sie verarbeitet. Diese Zeile wurde gelöscht, sodass die Abfrage von S2 fortgesetzt wird.

S2 kann während seines Abtastens der Tabelle zu der neuen Zeile gelangen oder nicht. Wenn dies der Fall ist, wird angezeigt, dass die neue Zeile nach dem DELETEStart der Anweisung von S2 erstellt wurde und daher nicht Teil des für sie sichtbaren Zeilensatzes ist.

Wenn PostgreSQL die gesamte DELETE-Anweisung von S2 von Anfang an mit einem neuen Snapshot neu starten würde, würde sie sich genauso verhalten wie SQL Server. Die PostgreSQL-Community hat dies aus Performancegründen nicht gewählt. In diesem einfachen Fall würden Sie den Unterschied in der Leistung nie bemerken, aber wenn Sie zehn Millionen Zeilen in einer DELETEZeile wären, als Sie blockiert wurden, würden Sie dies sicherlich tun. Hier gibt es einen Kompromiss, bei dem PostgreSQL die Leistung gewählt hat, da die schnellere Version immer noch den Anforderungen des Standards entspricht.

S2-2 wird ausgeführt und meldet eine eindeutige Verletzung der Schlüsselbedingung

Natürlich ist die Zeile bereits vorhanden. Dies ist der am wenigsten überraschende Teil des Bildes.

Obwohl es hier ein überraschendes Verhalten gibt, stimmt alles mit dem SQL-Standard überein und liegt im Rahmen dessen, was gemäß dem Standard "implementierungsspezifisch" ist. Es kann sicherlich überraschend sein, wenn Sie davon ausgehen, dass das Verhalten einer anderen Implementierung in allen Implementierungen vorhanden ist. PostgreSQL versucht jedoch, Serialisierungsfehler in der READ COMMITTEDIsolationsstufe zu vermeiden , und lässt einige Verhaltensweisen zu, die sich von anderen Produkten unterscheiden, um dies zu erreichen.

Ich persönlich bin kein großer Fan der READ COMMITTEDTransaktionsisolationsstufe in der Implementierung eines Produkts. Sie alle ermöglichen es den Rennbedingungen, aus transaktionaler Sicht überraschende Verhaltensweisen hervorzurufen. Sobald sich jemand an die seltsamen Verhaltensweisen eines Produkts gewöhnt, neigt er dazu, das "Normale" und die von einem anderen Produkt gewählten Kompromisse für ungerade zu halten. Aber jedes Produkt muss einen Kompromiss für einen Modus eingehen, der nicht als implementiert ist SERIALIZABLE. Die PostgreSQL-Entwickler haben sich für die READ COMMITTEDMinimierung der Blockierung (Lesevorgänge blockieren keine Schreibvorgänge und Schreibvorgänge blockieren keine Lesevorgänge) und die Minimierung der Wahrscheinlichkeit von Serialisierungsfehlern entschieden.

Der Standard verlangt, dass SERIALIZABLETransaktionen die Standardeinstellung sind, aber die meisten Produkte tun dies nicht, da dies zu Leistungseinbußen in Bezug auf die laxeren Transaktionsisolationsstufen führt. Einige Produkte bieten bei SERIALIZABLEAuswahl nicht einmal wirklich serialisierbare Transaktionen - insbesondere Oracle und Versionen von PostgreSQL vor 9.1. Die Verwendung von echten SERIALIZABLETransaktionen ist jedoch die einzige Möglichkeit, um überraschende Auswirkungen von Rennbedingungen zu vermeiden, und SERIALIZABLETransaktionen müssen immer entweder blockiert werden, um die Rennbedingungen zu vermeiden, oder einige Transaktionen zurückgesetzt werden, um eine sich entwickelnde Rennbedingung zu vermeiden. Die häufigste Implementierung von SERIALIZABLETransaktionen ist das strikte Zwei-Phasen-Sperren (S2PL), bei dem sowohl Blockierungs- als auch Serialisierungsfehler (in Form von Deadlocks) auftreten.

Vollständige Offenlegung: Ich habe mit Dan Ports vom MIT zusammengearbeitet, um PostgreSQL Version 9.1 mithilfe einer neuen Technik namens Serializable Snapshot Isolation wirklich serialisierbare Transaktionen hinzuzufügen.

kgrittn
quelle
Ich frage mich, ob eine wirklich billige (kitschige?) Möglichkeit, diese Arbeit zu machen, darin besteht, zwei DELETES gefolgt von der INSERT-Anweisung auszugeben. In meinen begrenzten (2 Threads) Tests hat es funktioniert, aber ich muss mehr testen, um zu sehen, ob das für viele Threads gilt.
DaveyBob
Solange Sie READ COMMITTEDTransaktionen verwenden, besteht eine Race-Bedingung: Was würde passieren, wenn eine andere Transaktion nach dem ersten DELETEund vor dem zweiten DELETEStart eine neue Zeile einfügt ? Bei Transaktionen weniger streng als SERIALIZABLEdie beiden wichtigsten Möglichkeiten , um enge Rennbedingungen sind durch Förderung eines Konflikts (aber das hilft nicht , wenn die Zeile gelöscht wird) und Materialisierung eines Konflikts. Sie können den Konflikt materialisieren, indem Sie für jede gelöschte Zeile eine "id" -Tabelle aktualisieren oder die Tabelle explizit sperren. Oder verwenden Sie Wiederholungsversuche bei Fehlern.
Kgrittn
Wiederholt es ist. Vielen Dank für den wertvollen Einblick!
DaveyBob
21

Ich glaube , das ist von Entwurf, nach der Beschreibung der Lese-engagierte Isolationsstufe für PostgreSQL 9.2:

Die Befehle UPDATE, DELETE, SELECT FOR UPDATE und SELECT FOR SHARE verhalten sich bei der Suche nach Zielzeilen wie SELECT: Sie finden nur Zielzeilen, die zum Zeitpunkt des Befehlsstarts festgeschrieben wurden 1 . Eine solche Zielzeile wurde jedoch möglicherweise bereits von einer anderen gleichzeitigen Transaktion aktualisiert (oder gelöscht oder gesperrt), als sie gefunden wurde. In diesem Fall wartet der potenzielle Updater, bis die erste Aktualisierungstransaktion festgeschrieben oder rückgängig gemacht wurde (sofern sie noch ausgeführt wird). Wenn der erste Updater einen Rollback durchführt, werden seine Auswirkungen annulliert und der zweite Updater kann mit der Aktualisierung der ursprünglich gefundenen Zeile fortfahren. Wenn der erste Updater einen Commit ausführt, ignoriert der zweite Updater die Zeile, wenn der erste Updater sie gelöscht hat. 2Andernfalls wird versucht, den Vorgang auf die aktualisierte Version der Zeile anzuwenden.

Die Zeile, in S1die Sie einfügen S2, war zu DELETEBeginn noch nicht vorhanden . Es wird also nicht durch das Löschen in S2gemäß ( 1 ) oben gesehen. Das S1gelöschte wird von S2's DELETEgemäß ( 2 ) .

Also S2, tut das Löschen nichts. Wenn der Einsatz allerdings kommt, dass man tut sehen S1ist , einfügen:

Da der Read Committed-Modus jeden Befehl mit einem neuen Snapshot startet , der alle bis zu diesem Zeitpunkt festgeschriebenen Transaktionen enthält, werden nachfolgende Befehle in derselben Transaktion in jedem Fall die Auswirkungen der festgeschriebenen gleichzeitigen Transaktion sehen . Der Punkt, um den es oben geht, ist, ob ein einzelner Befehl eine absolut konsistente Ansicht der Datenbank sieht oder nicht.

Also das versuchte Einfügen von S2 schlägt mit der Einschränkungsverletzung fehl.

Lesen Sie das Dokument weiter, verwenden Sie wiederholbares Lesen oder können Sie es sogar serialisieren würde Ihr Problem nicht vollständig lösen - die zweite Sitzung würde mit einem Serialisierungsfehler beim Löschen fehlschlagen.

Auf diese Weise können Sie die Transaktion erneut versuchen.

Matte
quelle
Danke Mat. Während das zu sein scheint, was passiert, scheint es einen Fehler in dieser Logik zu geben. Es scheint mir , dass in einem READ_COMMITTED ISO - Ebene, dann werden diese beiden Aussagen müssen in einem tx gelingen: DELETE FROM Test WHERE ID = 1 INSERT INTO Prüfwerte (1) Ich meine, wenn ich die Zeile löschen und legen Sie die Zeile, dann sollte diese Einfügung erfolgreich sein. SQLServer macht das richtig. Es fällt mir sehr schwer, mit dieser Situation in einem Produkt umzugehen, das mit beiden Datenbanken funktionieren muss.
DaveyBob
11

Ich stimme der hervorragenden Antwort von @ Mat voll und ganz zu . Ich schreibe nur eine andere Antwort, weil sie nicht in einen Kommentar passt.

Als Antwort auf Ihren Kommentar: DELETE in S2 ist bereits an eine bestimmte Zeilenversion angebunden. Da dies zwischenzeitlich von S1 getötet wird, sieht sich S2 als erfolgreich. Obwohl auf einen kurzen Blick nicht ersichtlich, sieht die Veranstaltungsreihe praktisch so aus:

   S1 DELETE erfolgreich  
S2 DELETE (erfolgreich per Proxy - DELETE von S1)  
   S1 fügt den gelöschten Wert in der Zwischenzeit virtuell wieder ein  
S2 INSERT schlägt mit Verletzung der eindeutigen Schlüsseleinschränkung fehl

Es ist alles beabsichtigt. Sie müssen wirklich SERIALIZABLETransaktionen für Ihre Anforderungen verwenden und sicherstellen, dass Sie es bei einem Serialisierungsfehler erneut versuchen.

Erwin Brandstetter
quelle
1

Verwenden Sie einen DEFERRABLE- Primärschlüssel und versuchen Sie es erneut.

Frank Heikens
quelle
danke für den tipp, aber die verwendung von DEFERRABLE machte überhaupt keinen unterschied. Das Dokument liest sich so, wie es sein sollte, tut es aber nicht.
DaveyBob
-2

Wir waren auch mit diesem Problem konfrontiert. Unsere Lösung fügt select ... for updatevor delete from ... where. Die Isolationsstufe muss Read Commit sein.

Mian Huang
quelle