Ich habe einen Webdienst (http api), mit dem ein Benutzer eine Ressource in Ruhe erstellen kann. Nach der Authentifizierung und Validierung übergebe ich die Daten an eine Postgres-Funktion und erlaube ihr, die Autorisierung zu überprüfen und die Datensätze in der Datenbank zu erstellen.
Ich habe heute einen Fehler gefunden, als zwei http-Anfragen innerhalb derselben Sekunde gestellt wurden, wodurch diese Funktion zweimal mit identischen Daten aufgerufen wurde. Innerhalb der Funktion gibt es eine Klausel, die eine Auswahl in einer Tabelle vornimmt, um festzustellen, ob ein Wert vorhanden ist. Wenn er vorhanden ist, nehme ich die ID und verwende sie bei meiner nächsten Operation. Wenn dies nicht der Fall ist, füge ich die Daten ein Sichern Sie die ID und verwenden Sie diese bei der nächsten Operation. Unten ist ein einfaches Beispiel.
select id into articleId from articles where title = 'my new blog';
if articleId is null then
insert into articles (title, content) values (_title, _content)
returning id into articleId;
end if;
-- Continue, using articleId to represent the article for next operations...
Wie Sie wahrscheinlich erraten können, habe ich ein Phantom auf die Daten gelesen, bei denen beide Transaktionen in den if articleId is null then
Block eingegeben und versucht haben, sie in die Tabelle einzufügen. Einer war erfolgreich und der andere explodierte aufgrund einer einzigartigen Einschränkung für ein Feld.
Ich habe mich umgesehen, wie ich mich dagegen verteidigen kann, und ein paar verschiedene Optionen gefunden, aber keine scheint aus einigen Gründen unseren Bedürfnissen zu entsprechen, und ich habe Mühe, Alternativen zu finden.
insert ... on conflict do nothing/update...
Ich habe mir zuerst dieon conflict
Option angesehen, die gut aussah, aber die einzige Option ist,do nothing
die dann nicht die ID des Datensatzes zurückgibt, der die Kollision verursacht hat, unddo update
nicht funktioniert, da dies dazu führt, dass Trigger ausgelöst werden, wenn in Wirklichkeit die Daten vorliegen hat sich nicht geändert. In einigen Fällen ist dies kein Problem, aber in vielen Fällen können Sitzungen Benutzersitzungen ungültig machen, was wir nicht tun können.set transaction isolation level serializable;
Dies scheint die attraktivste Antwort zu sein, aber selbst unsere Testsuite kann Lese- / Schreibabhängigkeiten verursachen, bei denen wir wie oben einfügen möchten, wenn etwas nicht vorhanden ist, und es zurückgeben möchten, wenn dies der Fall ist, und weitere Vorgänge fortsetzen möchten. Wenn mehrere Transaktionen anstehen, die den obigen Code ausführen, führt dies zu einem Lese- / Schreibabhängigkeitsfehler, wie in der Transaktions-ISO der Postgres-Dokumente beschrieben .
Wie soll diese Art von gleichzeitiger Lese- / Schreibtransaktion behandelt werden?
Weder ich noch mein Team behaupten, Datenbankexperten zu sein, geschweige denn Postgres-Experten, aber ich bin der Meinung, dass dies ein gelöstes Problem sein muss oder dass eine Person in der Vergangenheit auf sie gestoßen ist. Wir sind offen für Vorschläge. Wenn die oben angegebenen Informationen nicht ausreichen, kommentieren Sie dies bitte und ich werde bei Bedarf weitere Informationen hinzufügen.
quelle
if new is not distinct from old then return new; end if;
an die Spitze aller Aktualisierungsauslöser.Antworten:
Versuchen Sie das
insert
erste miton conflict ... do nothing
undreturning id
. Wenn der Wert bereits vorhanden ist, erhalten Sie kein Ergebnis dieser Anweisung. Sie müssen dann a ausführenselect
, um die ID zu erhalten.Wenn zwei Transaktionen dies gleichzeitig versuchen, blockiert eine von ihnen die
insert
(da die Datenbank noch nicht weiß, ob die andere Transaktion festgeschrieben oder zurückgesetzt wird) und fährt erst fort, nachdem die andere Transaktion abgeschlossen wurde.quelle
SELECT
, die eine gleichzeitige Transaktion bereits gelöscht hat (jedoch nicht festgeschrieben wurde). Bei starker gleichzeitiger Belastung müssen Sie möglicherweise mehr tun (Schleife), wie die anderen Antworten zeigen.Die
READ COMMITTED
Ursache des Problems liegt darin, dass mit der Standardisolationsstufe jedes gleichzeitige UPSERT (oder jede andere Abfrage) nur Zeilen sehen kann, die zu Beginn der Abfrage sichtbar waren. Das Handbuch:Ein
UNIQUE
Index ist jedoch absolut und muss gleichzeitig eingegebene Zeilen berücksichtigen - auch noch unsichtbare Zeilen. Sie können also eine Ausnahme für eine eindeutige Verletzung erhalten, aber die widersprüchliche Zeile in derselben Abfrage immer noch nicht sehen . Das Handbuch:Die Brute-Force- "Lösung" für dieses Problem besteht darin, widersprüchliche Zeilen mit zu überschreiben
ON CONFLICT ... DO UPDATE
. Die neue Zeilenversion ist dann in derselben Abfrage sichtbar. Aber es gibt mehrere Nebenwirkungen und ich würde davon abraten. Eine davon ist, dassUPDATE
Auslöser ausgelöst werden - das, was Sie ausdrücklich vermeiden möchten. Eng verwandte Antwort auf SO:Die verbleibende Möglichkeit ist , einen neuen Befehl zu starten (in derselben Transaktion), die dann sehen diese widersprüchlichen Zeilen aus der vorherige Abfrage. Beide vorhandenen Antworten legen dies nahe. Das Handbuch noch einmal:
Aber du willst mehr :
Wenn gleichzeitige Schreibvorgänge die Zeile möglicherweise ändern oder löschen können, müssen Sie die ausgewählte Zeile auch sperren , um absolut sicher zu sein . (Die eingefügte Zeile ist trotzdem gesperrt.)
Und da Sie anscheinend sehr wettbewerbsfähige Transaktionen haben, sollten Sie eine Schleife bis zum Erfolg durchführen , um sicherzustellen, dass Sie erfolgreich sind . Eingehüllt in eine plpgsql-Funktion:
Ausführliche Erklärung:
quelle
Ich denke, die beste Lösung besteht darin, nur die Einfügung vorzunehmen, den Fehler abzufangen und richtig damit umzugehen. Wenn Sie bereit sind, Fehler zu behandeln, ist eine serialisierbare Isolationsstufe (anscheinend) für Ihren Fall nicht erforderlich. Wenn Sie nicht auf Fehler vorbereitet sind, hilft die serialisierbare Isolationsstufe nicht - es entstehen nur noch mehr Fehler, auf die Sie nicht vorbereitet sind.
Eine andere Möglichkeit wäre, ON CONFLICT DO NOTHING auszuführen. Wenn dann nichts passiert, führen Sie anschließend die Abfrage aus, die Sie bereits ausführen, um den Wert zu erhalten, der jetzt vorhanden sein muss. Mit anderen Worten, wechseln Sie
select id into articleId from articles where title = 'my new blog';
von einem vorbeugenden Schritt zu einem Schritt, der nur ausgeführt wird, wenn ON CONFLICT DO NOTHING tatsächlich nichts tut. Wenn es möglich ist, einen Datensatz einzufügen und dann wieder zu löschen, sollten Sie dies in einer Wiederholungsschleife tun.quelle