Race-Condition: Wie Doppeleinträge verhindern?

Hallo!

Wenn ich in einer Tabelle eine existierende Zeile habe, die ich exklusiv verändern will, kann ich ja sehr schön “…for update” nutzen.

Was ist aber bei folgendem Szenario:
Ich möchte eine Zeile in eine Tabelle einfügen, aber sicherstellen, dass der Datensatz in dieser Form noch NICHT existiert. “for update” nützt mir hier relativ wenig, da es ja nur einen Lock auf existierende Zeilen vergibt.

Gut, ich könnte mit einem Unique-Key arbeiten, der über mehrere Felder geht, um das zu verhindern, aber das erzeugt dann einen Fehler bei einem konkurrierenden INSERT, was ich eigentlich vermeiden will. Es muss da doch eine “herkömmliche” Lösung geben…?

Macht man es auf “normale” Weise, kann halt im ungünstigsten Fall eine Race-Condition entstehen:
Thread 1: select id from table where feld1=xy and feld2=zz;
Thread 2: select id from table where feld1=xy and feld2=zz;
Thread 1: Stellt fest, dass die Zeile nicht existiert und bereitet das INSERT vor
Thread 2: Stellt das gleiche fest
Thread 1: INSERT
Thread 2: INSERT
Ergebnis: Doppeleintrag

Wenn ich das richtig verstanden habe, nützt mir eine Transaktion hier nichts, da ja z.B. bei 2 selects in einer Transaktion unterschiedliche Ergebnisse geliefert werden können (“nonrepeatable reads”), da ja durch die Transaktion selbst erstmal kein Lock vergeben wird.

Wie stelle ich also bei einem INSERT sicher, dass die Zeile noch nicht existiert, ohne dass mir ein konkurrierender Thread dazwischenfunken kann?

Du könntest dich streng an den SQL-Standard halten, dann schraubst du den Isolationslevel hoch und eine deiner beiden Anfragen schlägt fehl. Diese wiederholst du. Das ist der laut Standard vorgeschlagene Weg.

Eine zweite Möglichkeit wäre das du einen LOCK auf die gesamte Tabelle legst, dann kann dir keine zweite Verbindung dazwischen schreiben.

Oder du schaust dir mal an, ob dir das hier weiterhilft:

23:14:15 < ads> ??upsert
23:14:17 < pg_docbot_adz> For information about ‘upsert’ see:
23:14:17 < pg_docbot_adz> > PostgreSQL: Documentation: Manuals: PostgreSQL 8.3: Control Structures

Hmm, das Hochschrauben des Isolationlevels betrifft ja die gesamte DB…
“The Serializable mode provides a rigorous guarantee that each transaction sees a wholly consistent view of the database.”

Klingt etwas unperformant.
Ich denke, ein Table Lock ist hier die richtige Wahl.

der Unique-Key ist der richtige Weg, denn entweder das Modell sieht vor, dass die entsprechenden Einträge Unique sind - oder eben nicht.

Außerdem schlägt das INSERT erst fehl wenn du es commitest (isolationslevel Read committed) - es ist mir nicht ganz klar, was du unter “konurrierenden INSERTS” verstehst. Ich verstehe auch nicht, warum du INSERTS in einer Weise zulassen willst, die ohne constraint zu Inkonsistenzen führen.

Man muss SELECT … FOR UPDATE ja nicht verwenden, wenn man Locks vermeiden moechte … oder auch garnicht benötigt … Im obigen Fall gewinnt eben einfach das erste COMMIT. Das Zweite kriegt nen Fehler - was ja auch korrekt ist - daraufhin kannst dann reagieren.

Das sind Fragen die sollte man sich stellen (zumindest ich Dir :slight_smile: )