Jeder kennt es, keiner will es: Race-Conditions. Ein Benutzer registriert sich und versendet versehentlich zweimal hintereinander das gleiche Formular. Wer jetzt zuerst zählt:

SELECT COUNT(*) AS total FROM tabelle WHERE mail = 'user@example.com';

und danach gleich einfügt:

INSERT INTO tabelle (mail) VALUES ('user@example.com');

läuft Gefahr Opfer einer Race-Condition zu werden.

Was ist das und was passiert genau?

Das ist eigentlich ganz einfach erklärt. Wenn wir zweimal das gleiche Formular versenden, durchläuft der Server zweimal die gleiche Datei. Ist der zeitliche Unterschied zwischen den beiden Aufrufen genug klein, ergibt sich bei beiden bei der ersten Zählung 0 und es wird danach auch bei beiden eingefügt. Da Spalten wie “mail” aber üblicherweise einen “unique-constraint” oder eben einen “unique-functional-index” besitzen, kommt es zum schlimmsten: Der spätere der beiden Aufrufe schlägt fehl und wird mit einem Fehler quittiert: Der Datensatz ist bereits vorhanden.

Aber abgebrochene Aufrufe werden vom Server ja auch abgebrochen?

Mit den Standardeinstellungen schon. Oftmals entscheiden sich Programmierer aber dazu “ignore_user_abort” zu benutzen was ja auch seinen guten Grund hat. Beispiel Bilderupload: Dort wird üblicherweise ein SQL-Query versandt und danach die Datei im Dateisystem entsprechend untergebracht. (Das ganze geht auch umgekehrt) Wenn der Benutzer jetzt zwischen einer der beiden Aktionen seinen Aufruf abbricht, haben wir einen losen Datensatz in der Datenbank oder im Dateisystem. Die Integrität wäre verloren. Ein weiteres Beispiel wäre eine Tabelle in der alle Besuche eines Mitglieds bei einem anderen Mitglied gespeichert werden natürlich mit einem “unique-constraint” über from_id und dest_id:

UPDATE tabelle SET visited = NOW() WHERE from_id = 1 AND dest_id = 2;

Wer sich jetzt nur auf die betroffenen Zeilen verlässt und danach gleich einfügt:

INSERT INTO tabelle (from_id, dest_id, visited) VALUES (1, 2, NOW());

hat wieder eine wunderschöne und noch viel wahrscheinlich eintreffendere Race-Condition erstellt. Bei diesem Beispiel reicht es sogar aus ein Mitglied, welches noch nie besucht wurde, zweimal schnell hintereinander (Doppelklick) zu besuchen was ja nun wirklich nicht realitätsfremd ist.

Was kann ich dagegen tun?

Das ist jetzt eben der trickreiche Teil des ganzen. Er nennt sich “INSERT INTO … SELECT” mit einem Subquery und sieht aus wie folgt:

INSERT INTO tabelle (mail) SELECT 'user@example.com' WHERE (SELECT COUNT(*) FROM tabelle WHERE mail = 'user@example.com') = 0;

und für das zweite Beispiel:

INSERT INTO tabelle (from_id, dest_id, visited) SELECT 1, 2, NOW() WHERE (SELECT COUNT(*) FROM tabelle WHERE from_id = 1 AND dest_id = 2) = 0;

Das war es auch schon. Wenn wir jetzt noch sauberkeitshalber die betroffenen Zeilen abfragen, können wir eine schön formatierte Meldung über den (Miss-)Erfolg ausgeben.