Mysql
 sql >> Datenbank >  >> RDS >> Mysql

MySQL-Update-Abfrage - Wird die 'where'-Bedingung bei Race-Bedingungen und Zeilensperren eingehalten? (php, PDO, MySQL, InnoDB)

Die Where-Bedingung wird während einer Rennsituation respektiert, aber Sie müssen vorsichtig sein, wie Sie überprüfen, wer das Rennen gewonnen hat.

Betrachten Sie die folgende Demonstration, wie dies funktioniert und warum Sie vorsichtig sein müssen.

Richten Sie zunächst einige Minimaltabellen ein.

CREATE TABLE table1 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY,
`locked` TINYINT UNSIGNED NOT NULL,
`updated_by_connection_id` TINYINT UNSIGNED DEFAULT NULL
) ENGINE = InnoDB;

CREATE TABLE table2 (
`id` TINYINT UNSIGNED NOT NULL PRIMARY KEY
) ENGINE = InnoDB;

INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

id spielt die Rolle von id in Ihrer Tabelle updated_by_connection_id verhält sich wie assignedPhone , und locked wie reservationCompleted .

Beginnen wir nun mit dem Renntest. Sie sollten 2 Befehlszeilen-/Terminalfenster geöffnet haben, die mit mysql verbunden sind und die Datenbank verwenden, in der Sie diese Tabellen erstellt haben.

Verbindung 1

start transaction;

Verbindung 2

start transaction;

Verbindung 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

Verbindung 2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

Verbindung 2 wartet nun

Verbindung 1

SELECT * FROM table1 WHERE id = 1;
commit;

An diesem Punkt wird Verbindung 2 zum Fortfahren freigegeben und gibt Folgendes aus:

Verbindung 2

SELECT * FROM table1 WHERE id = 1;
commit;

Alles sieht gut aus. Wir sehen, dass ja, die WHERE-Klausel in einer Race-Situation respektiert wurde.

Der Grund, warum ich gesagt habe, dass Sie vorsichtig sein müssen, ist, dass die Dinge in einer echten Anwendung nicht immer so einfach sind. Innerhalb der Transaktion KÖNNEN andere Aktionen ausgeführt werden, die die Ergebnisse tatsächlich ändern können.

Setzen wir die Datenbank wie folgt zurück:

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

Betrachten Sie nun diese Situation, in der vor dem UPDATE ein SELECT durchgeführt wird.

Verbindung 1

start transaction;

SELECT * FROM table2;

Verbindung 2

start transaction;

SELECT * FROM table2;

Verbindung 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;

Verbindung 2

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;

Verbindung 2 wartet nun

Verbindung 1

SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

An diesem Punkt wird Verbindung 2 zum Fortfahren freigegeben und gibt Folgendes aus:

Okay, mal sehen, wer gewonnen hat:

Verbindung 2

SELECT * FROM table1 WHERE id = 1;

Warte was? Warum ist locked 0 und updated_by_connection_id NULL??

Das ist die Vorsicht, die ich erwähnt habe. Der Schuldige liegt eigentlich daran, dass wir am Anfang eine Auswahl getroffen haben. Um das richtige Ergebnis zu erhalten, könnten wir Folgendes ausführen:

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Durch die Verwendung von SELECT ... FOR UPDATE können wir das richtige Ergebnis erhalten. Dies kann sehr verwirrend sein (wie es ursprünglich für mich war), da ein SELECT und ein SELECT ... FOR UPDATE zwei unterschiedliche Ergebnisse liefern.

Der Grund dafür ist die Standard-Isolationsstufe READ-REPEATABLE . Wenn das erste SELECT gemacht wird, direkt nach dem start transaction; , wird ein Snapshot erstellt. Alle zukünftigen nicht aktualisierenden Lesevorgänge werden von diesem Snapshot durchgeführt.

Wenn Sie also nach der Aktualisierung einfach naiv AUSWÄHLEN, werden die Informationen aus diesem ursprünglichen Snapshot gezogen, der vorher war Die Zeile wurde aktualisiert. Indem Sie ein SELECT ... FOR UPDATE ausführen, zwingen Sie es, die richtigen Informationen zu erhalten.

In einer realen Anwendung könnte dies jedoch wiederum ein Problem sein. Angenommen, Ihre Anfrage ist in eine Transaktion eingeschlossen, und Sie möchten nach der Aktualisierung einige Informationen ausgeben. Das Sammeln und Ausgeben dieser Informationen kann durch separaten, wiederverwendbaren Code gehandhabt werden, den Sie NICHT mit FOR UPDATE-Klauseln "nur für den Fall" verunreinigen möchten. Das würde zu viel Frust durch unnötiges Sperren führen.

Stattdessen sollten Sie eine andere Spur nehmen. Sie haben hier viele Möglichkeiten.

Zum einen müssen Sie sicherstellen, dass Sie die Transaktion festschreiben, nachdem das UPDATE abgeschlossen ist. In den meisten Fällen ist dies wahrscheinlich die beste und einfachste Wahl.

Eine andere Möglichkeit besteht darin, nicht zu versuchen, SELECT zu verwenden, um das Ergebnis zu bestimmen. Stattdessen können Sie möglicherweise die betroffenen Zeilen lesen und diese verwenden (1 Zeile aktualisiert vs. 0 Zeilen aktualisiert), um festzustellen, ob das UPDATE erfolgreich war.

Eine weitere Option, die ich häufig verwende, da ich gerne eine einzelne Anfrage (wie eine HTTP-Anfrage) vollständig in einer einzigen Transaktion verpacken möchte, besteht darin, sicherzustellen, dass die erste in einer Transaktion ausgeführte Anweisung entweder UPDATE ist oder ein SELECT ... FOR UPDATE . Dadurch wird der Snapshot NICHT erstellt, bis die Verbindung fortgesetzt werden darf.

Lassen Sie uns unsere Testdatenbank erneut zurücksetzen und sehen, wie das funktioniert.

delete from table1;
INSERT INTO table1
(`id`,`locked`)
VALUES
(1,0);

Verbindung 1

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

Verbindung 2

start transaction;

SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

Verbindung 2 wartet nun.

Verbindung 1

UPDATE table1
SET locked = 1,
updated_by_connection_id = 1
WHERE id = 1
AND locked = 0;
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Verbindung 2 ist jetzt freigegeben.

Verbindung 2

+----+--------+--------------------------+
| id | locked | updated_by_connection_id |
+----+--------+--------------------------+
|  1 |      1 |                        1 |
+----+--------+--------------------------+

Hier könnten Sie Ihren serverseitigen Code tatsächlich die Ergebnisse dieses SELECT überprüfen lassen und wissen, dass es korrekt ist, und nicht einmal mit den nächsten Schritten fortfahren. Aber der Vollständigkeit halber werde ich wie zuvor abschließen.

UPDATE table1
SET locked = 1,
updated_by_connection_id = 2
WHERE id = 1
AND locked = 0;
SELECT * FROM table1 WHERE id = 1;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
commit;

Jetzt können Sie sehen, dass in Connection 2 SELECT und SELECT ... FOR UPDATE das gleiche Ergebnis liefern. Dies liegt daran, dass der Snapshot, aus dem SELECT liest, erst erstellt wurde, nachdem Verbindung 1 festgeschrieben wurde.

Also zurück zu Ihrer ursprünglichen Frage:Ja, die WHERE-Klausel wird in allen Fällen von der UPDATE-Anweisung überprüft. Sie müssen jedoch vorsichtig mit allen SELECTs sein, die Sie möglicherweise durchführen, um zu vermeiden, dass das Ergebnis dieses UPDATE falsch bestimmt wird.

(Ja, eine andere Möglichkeit ist, die Transaktionsisolationsstufe zu ändern. Ich habe jedoch nicht wirklich Erfahrung damit und mit irgendwelchen Fallstricken, die existieren könnten, also werde ich nicht darauf eingehen.)