2016-08-26 1 views
2

Hier ist eine ordentliche Sperrung Problem mit MariaDB/MySQL.SQL-Tabelle Sperren Race-Bedingung - SELECT dann INSERT

Ein Server setzt mehrteilige SMS-Nachrichten zusammen. Nachrichten kommen in Segmenten an. Segmente mit den gleichen "smsfrom" und "uniqueid" sind Teil derselben Nachricht. Segmente haben eine Segmentnummer von 1 bis "segmenttotal". Wenn alle Segmente einer Nachricht angekommen sind, ist die Nachricht abgeschlossen. Wir haben eine Tabelle von unpassenden Segmente warten wieder aufgebaut werden, wie folgt:

CREATE TABLE frags (
     smsfrom TEXT, 
     uniqueid VARCHAR(32) NOT NULL, 
     smsbody TEXT, 
     segmentnum INTEGER NOT NULL, 
     segmenttotal INTEGER NOT NULL); 

Wenn ein neues Segment in kommt, was wir tun, im Rahmen einer Transaktion,

SELECT ... FROM frags WHERE smsfrom = % AND uniqueid = %; 

Dies wird uns alle Segmente empfangen bisher. Wenn die neue plus diese alle Segmentnummern hat, haben wir eine vollständige Nachricht. Wir senden die Nachricht zur weiteren Bearbeitung und löschen die betroffenen Fragmente. Fein.

Wenn noch nicht alle Segmente angekommen sind, machen wir einen INSERT des Segments, das wir gerade bekommen haben. Autocommit ist deaktiviert, daher sind beide Vorgänge Teil einer Transaktion. InnoDB-Engine übrigens.

Dies hat eine Race-Bedingung. Zwei Segmente kommen gleichzeitig für eine Zwei-Segment-Nachricht und werden von separaten Prozessen verarbeitet. Prozess A macht das SELECT, findet nichts. Prozess B macht das SELECT, findet nichts. Prozess A fügt Segment 1 ein, kein Problem. Prozess B fügt Segment 2 ein, kein Problem. Jetzt stecken wir fest - alle Segmente sind in der Tabelle, aber wir haben es nicht bemerkt. Die Nachricht bleibt also für immer dort stecken. (In der Praxis führen wir alle paar Minuten eine Bereinigung durch, um alte nicht übereinstimmende Daten zu entfernen, aber ignorieren Sie das jetzt.)

Also, was ist los? Die SELECTs sperren keine Zeilen, weil sie nichts finden. Wir brauchen eine Zeilensperre für eine Zeile, die noch nicht existiert. Das Hinzufügen von FOR UPDATE zu SELECT hilft nicht; nichts zu sperren. Es wird auch nicht im Freigabe-Modus gesperrt. Selbst wenn man zu einem Transaktionstyp von SERIALIZABLE geht, hilft das nicht, denn das ist nur der globale LOCK IN SHARE MODE.

OK, also nehmen wir zuerst die INSERT und dann eine SELECT, um zu sehen, ob wir alle Segmente haben. Prozess A macht den INSERT von 1, kein Problem. Prozess B macht den Einsatz von 2, kein Problem. Prozess A macht eine SELECT und sieht nur 1. Prozess B macht eine SELECT, und sieht nur 2. Das ist wiederholbare Lese-Semantik. Nicht gut.

Der Brute-Force-Ansatz ist eine LOCK TABLE, bevor Sie dies tun. Das sollte funktionieren, obwohl es nervig ist, weil ich in einer Transaktion bin, die andere Tabellen involviert und LOCK TABLE ein Commit beinhaltet.

Ein Commit nach jedem INSERT könnte funktionieren, aber ich bin mir nicht ganz sicher.

Gibt es eine elegantere Lösung?

+0

Was ist, wenn Sie Ihren Check für andere Segmente nach dem Commit verschoben haben? Sie müssen sicherstellen, dass Prozess A und B die Nachricht nicht verarbeiten. – Greg

Antwort

1

Warum nicht

1) Verfahren 1. Legen Sie In Ihrer Splitter Tabelle. Nichts anderes

Insert .... Commit;

2) Verfahren 2 Diese den vollständigen mehrteiliger SMS finden, indem

select smsfrom, einzigartig, uniqueid, count () aus frags Gruppe von smsfrom, einzigartig, einzigartig mit count () == segmenttotal;

sie in die neue Tabelle verschieben

aus frags streichen smsfrom = <> und einzigartig = <>;

commit;

+0

Ich habe so etwas gemacht - Einfügen, Festschreiben, dann in einem Prozess auswählen. Wählen Sie FOR UPDATE, um Zeilensperren gegen ein anderes SELECT zu erhalten, oder zwei Prozesse könnten beide die zusammengesetzte Nachricht finden und würden zweimal behandelt werden. –

+0

Sehr froh zu hören, dass hilft .... Ich werde nicht fragen, was Sie tun, um die OOB (Out of Band) Multipart-Nachrichten, die ankommen, nachdem Sie die Originale erfolgreich zusammengebaut :) - müssen Sie möglicherweise für sie Konto oder einfach löschen alles, was mehr als 2 Tage alt ist - es hängt von Ihren geschäftlichen Anforderungen ab. –

0

Wie ich oben schrieb, landete ich tun dies:

INSERT ... -- Insert new fragment. 
COMMIT 
SELECT ... FROM frags WHERE smsfrom = % AND uniqueid = % FOR UPDATE; 

Überprüfen Sie, ob die SELECT einen vollständigen Satz von Fragmenten zurückgegeben. Wenn ja, wieder zusammenbauen und Nachricht verarbeiten, dann

DELETE ... FROM FRAGS WHERE smsfrom = % AND uniqueid = %; 

Sowohl das COMMIT und das FOR UPDATE sind notwendig. Das COMMIT wird benötigt, damit jeder Prozess ein INSERT von einem anderen Prozess sieht. Das FOR UPDATE wird bei SELECT benötigt, um alle Fragmente zu sperren, bis DELETE ausgeführt werden kann. Andernfalls sehen zwei Prozesse möglicherweise die vollständige Gruppe von Fragmenten in der SELECT-Anweisung und bauen die Nachricht erneut zusammen und verarbeiten sie zweimal.

Dies ist überraschend kompliziert für ein Ein-Tabellen-Problem, scheint aber zu funktionieren.

Verwandte Themen