Sperren von Postgres für die Kombination UPDATE / INSERT

11

Ich habe zwei Tische. Eine ist eine Protokolltabelle; Ein anderer enthält im Wesentlichen Gutscheincodes, die nur einmal verwendet werden können.

Der Benutzer muss in der Lage sein, einen Gutschein einzulösen, der eine Zeile in die Protokolltabelle einfügt und den Gutschein als verwendet markiert (durch Aktualisieren der usedSpalte auf true).

Natürlich gibt es hier ein offensichtliches Problem mit der Rennbedingung / Sicherheit.

Ich habe in der Vergangenheit ähnliche Dinge in der Welt von mySQL getan. In dieser Welt würde ich beide Tabellen global sperren, die Logik in dem Wissen sicher machen, dass dies nur einmal gleichzeitig passieren kann, und dann die Tabellen entsperren, sobald ich fertig bin.

Gibt es in Postgres einen besseren Weg, dies zu tun? Insbesondere mache ich mir Sorgen, dass die Sperre global ist, aber nicht sein muss - ich muss wirklich nur sicherstellen, dass niemand anderes versucht, diesen bestimmten Code einzugeben, sodass möglicherweise eine Sperre auf Zeilenebene funktioniert?

Rob Miller
quelle

Antworten:

14

Ich habe schon einmal von solchen Parallelitätsproblemen in MySQL gehört. Nicht so bei Postgres.

Einbau-Zeilenebene Sperren in der READ COMMITTEDStandardtransaktionsisolationsstufe genug sind.

Ich schlage eine einzelne Anweisung mit einem datenmodifizierenden CTE vor (etwas, das MySQL auch nicht hat), da es praktisch ist, Werte direkt von einer Tabelle zur anderen zu übergeben (falls Sie dies benötigen sollten). Wenn Sie nichts aus der couponTabelle benötigen, können Sie auch eine Transaktion mit separaten Anweisungen UPDATEund INSERTAnweisungen verwenden.

WITH upd AS (
   UPDATE coupon
   SET    used = true
   WHERE  coupon_id = 123
   AND    NOT used
   RETURNING coupon_id, other_column
   )
INSERT INTO log (coupon_id, other_column)
SELECT coupon_id, other_column FROM upd;

Es sollte selten vorkommen , dass mehr als eine Transaktion versucht, denselben Gutschein einzulösen. Sie haben eine eindeutige Nummer, nicht wahr? Mehr als eine Transaktion, die gleichzeitig versucht wird, sollte jedoch viel seltener sein. (Vielleicht ein Anwendungsfehler oder jemand, der versucht, das System zu spielen?)

Wie dem auch sei, die UPDATEeinzige ist für genau eine Transaktion erfolgreich, egal was passiert. A UPDATEerwirbt vor dem Aktualisieren eine Sperre auf Zeilenebene für jede Zielzeile. Wenn eine gleichzeitige Transaktion versucht, UPDATEdieselbe Zeile zu bearbeiten, wird die Sperre für die Zeile angezeigt und es wird gewartet, bis die blockierende Transaktion abgeschlossen ist ( ROLLBACKoder COMMIT). Dies ist die erste in der Sperrwarteschlange:

  • Wenn festgeschrieben, überprüfen Sie die Bedingung erneut. Wenn es immer noch ist NOT used, sperren Sie die Zeile und fahren Sie fort. Andernfalls findet der UPDATEjetzt keine qualifizierende Zeile und tut nichts , gibt keine Zeile zurück, also INSERTtut der auch nichts.

  • Wenn Sie einen Rollback durchführen, sperren Sie die Zeile und fahren Sie fort.

Es gibt kein Potenzial für eine Rennbedingung .

Es besteht kein Potenzial für einen Deadlock, es sei denn, Sie schreiben mehr Schreibvorgänge in dieselbe Transaktion oder sperren auf andere Weise mehr Zeilen als nur eine.

Das INSERTist sorglos. Wenn aus Versehen das coupon_idbereits in der logTabelle enthalten ist (und Sie eine EINZIGARTIGE oder PK-Einschränkung haben log.coupon_id), wird die gesamte Transaktion nach einer eindeutigen Verletzung zurückgesetzt. Würde auf einen illegalen Status in Ihrer Datenbank hinweisen. Wenn die obige Anweisung die einzige Möglichkeit ist, in die logTabelle zu schreiben , sollte dies niemals vorkommen.

Erwin Brandstetter
quelle
Es sollte in der Tat selten vorkommen, dass mehr als eine Transaktion versucht, denselben Code einzulösen, aber Ihr Verdacht ist insofern richtig, als dies ausschließlich dann der Fall sein wird, wenn jemand versucht, das System zu spielen. Vielen Dank dafür - CTEs waren ein großer Anziehungspunkt für mich, als ich zu Postgres wechselte, aber ich wusste nicht, dass implizites Sperren dafür gut genug wäre.
Rob Miller