Was ist genau gesperrt, wenn Sie die FOR UPDATE-Sperre von MySQL verwenden?

77

Dies ist kein vollständiger / korrekter MySQL-Abfrage-Pseudocode:

Select *
 from Notifications as n
 where n.date > (CurrentDate-10 days)
 limit by 1
 FOR UPDATE

http://dev.mysql.com/doc/refman/5.0/en/select.html besagt: Wenn Sie FOR UPDATE mit einer Speicher-Engine verwenden, die Seiten- oder Zeilensperren verwendet, werden die von der Abfrage untersuchten Zeilen bis zum Ende der aktuellen Transaktion

Ist hier nur der eine von MySQL zurückgegebene Datensatz gesperrt oder alle Datensätze, die gescannt werden müssen, um den einzelnen Datensatz zu finden?

Markus
quelle
Das Sperren all Records it has to SCAN TO FIND the SINGLE RECORDwäre so schrecklich dumm, dass ich wirklich bezweifle, dass MySQL so funktioniert. Denken Sie an den Algorithmus in der MySQL-Suchmaschine - wenn er eine Zeile sieht und weiß, dass es nicht die Zeile ist, die Sie benötigen, warum sollte er dann zusätzliche Zeit für das Setzen der Sperre aufwenden?! Ich schlage vor, dass Sie die Antwort nicht akzeptieren, damit andere MySQL-Leute dies kommentieren können
Alexander Malakhov
Als Oracle DB-Entwickler versichere ich Ihnen außerdem, dass Oracle nur Zeilen sperrt, die die WHEREBedingung erfüllen . Daher ist es technisch möglich und ich denke nicht, dass MySQL so viel minderwertiger ist
Alexander Malakhov
1
Obwohl es so aussieht, als ob meine Antwort richtig war, schlage ich vor, dass Sie stattdessen die andere Antwort auswählen, da sie korrekt ist und sie tatsächlich getestet hat, während meine nur auf die Dokumentation verweist, die, wie Alexander betont, in mehr als gelesen werden könnte Einweg.
El Yobo

Antworten:

109

Warum versuchen wir es nicht einfach?

Richten Sie die Datenbank ein

CREATE DATABASE so1;
USE so1;
CREATE TABLE notification (`id` BIGINT(20), `date` DATE, `text` TEXT) ENGINE=InnoDB;
INSERT INTO notification(id, `date`, `text`) values (1, '2011-05-01', 'Notification 1');
INSERT INTO notification(id, `date`, `text`) values (2, '2011-05-02', 'Notification 2');
INSERT INTO notification(id, `date`, `text`) values (3, '2011-05-03', 'Notification 3');
INSERT INTO notification(id, `date`, `text`) values (4, '2011-05-04', 'Notification 4');
INSERT INTO notification(id, `date`, `text`) values (5, '2011-05-05', 'Notification 5');

Starten Sie nun zwei Datenbankverbindungen

Verbindung 1

BEGIN;
SELECT * FROM notification WHERE `date` >= '2011-05-03' FOR UPDATE;

Verbindung 2

BEGIN;

Wenn MySQL alle Zeilen sperrt, wird die folgende Anweisung blockiert. Wenn nur die zurückgegebenen Zeilen gesperrt werden, sollte es nicht blockiert werden.

SELECT * FROM notification WHERE `date` = '2011-05-02' FOR UPDATE;

Und tatsächlich blockiert es.

Interessanterweise können wir auch keine Datensätze hinzufügen, die gelesen werden würden, d. H.

INSERT INTO notification(id, `date`, `text`) values (6, '2011-05-06', 'Notification 6');

Blöcke auch!

Ich kann zum jetzigen Zeitpunkt nicht sicher sein, ob MySQL nur die gesamte Tabelle sperrt, wenn ein bestimmter Prozentsatz der Zeilen gesperrt ist, oder ob es wirklich intelligent ist, um sicherzustellen, dass das Ergebnis der SELECT ... FOR UPDATEAbfrage niemals durch eine andere Transaktion geändert werden kann ( mit einem INSERT,, UPDATEoder DELETE), während das Schloss gehalten wird.

Frans
quelle
6
+1 als Beweis durch Beispiel. Dieses Verhalten sieht erschreckend aus. Haben Sie versucht, nur 1 Zeile zu sperren? Warum wählst du nicht nach idSpalten aus, ich vertraue Datumsliteralen nicht :). Was ist die Version von MySQL / InnoDB?
Alexander Malakhov
42
+1. Die gesamte Tabelle wird nicht blockiert, wenn eine eindeutige Spalte gesperrt wird. Ich habe versucht, CREATE TABLE notification (id` BIGINT (20) NICHT NULL AUTO_INCREMENT, dateDATE, textTEXT, PRIMARY KEY ( id)) ENGINE = InnoDB; `was funktioniert. Es funktioniert jedoch nicht, wenn kein eindeutiger / Primärschlüssel angegeben ist, z. SELECT * FROM notification WHERE id` = '1' FOR UPDATE; `auf dem ursprünglichen Schema
deepak
6
@fabspro Sie müssen keinen eindeutigen Schlüssel verwenden. Jeder Schlüssel funktioniert, egal ob er eindeutig ist oder nicht.
thekingoftruth
3
Meine eigenen Tests zeigen, dass die Verwendung for updatevon where-Filtern für nicht indizierte Spalten zu einer Sperre der gesamten Tabelle führt, während die Verwendung von where-Filtern für indizierte Spalten zu dem gewünschten Verhalten der gefilterten Zeilensperrung führt. Ein Primärschlüssel ist also nicht erforderlich, jeder Schlüssel reicht aus.
CMCDragonkai
2
Wenn Sie jedoch bei weiteren Tests mit einem Index for updateeinen where-Filter ausführen , der zu einem leeren Satz führt, wird dadurch keine weitere Abfrage für denselben leeren Satz blockiert. Während ohne Index die Ausführung for updatedie Abfrage für denselben leeren Satz blockiert.
CMCDragonkai
26

Ich weiß, dass diese Frage ziemlich alt ist, aber ich wollte die Ergebnisse einiger relevanter Tests, die ich mit indizierten Spalten durchgeführt habe, teilen, was zu ziemlich seltsamen Ergebnissen geführt hat.

Tabellenstruktur:

CREATE TABLE `t1` (                       
  `id` int(11) NOT NULL AUTO_INCREMENT,                 
  `notid` int(11) DEFAULT NULL,                         
  PRIMARY KEY (`id`)                                    
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

12 Zeilen eingefügt mit INSERT INTO t1 (notid) VALUES (1), (2),..., (12). Bei Verbindung 1 :

BEGIN;    
SELECT * FROM t1 WHERE id=5 FOR UPDATE;

Bei Verbindung 2 werden folgende Anweisungen blockiert:

SELECT * FROM t1 WHERE id!=5 FOR UPDATE;
SELECT * FROM t1 WHERE id<5 FOR UPDATE;
SELECT * FROM t1 WHERE notid!=5 FOR UPDATE;
SELECT * FROM t1 WHERE notid<5 FOR UPDATE;
SELECT * FROM t1 WHERE id<=4 FOR UPDATE;

Das Merkwürdigste ist , dass SELECT * FROM t1 WHERE id>5 FOR UPDATE;wird nicht blockiert , noch sind irgendwelche

...
SELECT * FROM t1 WHERE id=3 FOR UPDATE;
SELECT * FROM t1 WHERE id=4 FOR UPDATE;
SELECT * FROM t1 WHERE id=6 FOR UPDATE;
SELECT * FROM t1 WHERE id=7 FOR UPDATE;
...

Ich möchte auch darauf hinweisen, dass anscheinend die gesamte Tabelle gesperrt ist, wenn die WHEREBedingung in der Abfrage von Verbindung 1 mit einer nicht indizierten Zeile übereinstimmt. Wenn beispielsweise Verbindung 1 ausgeführt wird SELECT * FROM t1 WHERE notid=5 FOR UPDATE, werden alle ausgewählten Abfragen mit FOR UPDATEund UPDATEAbfragen von Verbindung 2 blockiert.

- BEARBEITEN -

Dies ist eine ziemlich spezifische Situation, aber es war die einzige, die ich finden konnte, die dieses Verhalten zeigt:

Verbindung 1:

BEGIN;
SELECT *, @x:=@x+id AS counter FROM t1 CROSS JOIN (SELECT @x:=0) b HAVING counter>5 LIMIT 1 FOR UPDATE;
+----+-------+-------+---------+
| id | notid | @x:=0 | counter |
+----+-------+-------+---------+
|  3 |     3 |     0 |       9 |
+----+-------+-------+---------+
1 row in set (0.00 sec)

Ab Verbindung 2 :

SELECT * FROM t1 WHERE id=2 FOR UPDATE; ist blockiert;

SELECT * FROM t1 WHERE id=4 FOR UPDATE;ist nicht blockiert.

concat
quelle
das ist interessant. Ich möchte wissen, warum diese Blockaden auch passieren.
Todeskaiser
Es liegt an Lückensperren, die auf Indizes basieren.
M Rostami
16

Der Thread ist ziemlich alt, nur um meine zwei Cent in Bezug auf die oben von @Frans durchgeführten Tests zu teilen

Verbindung 1

BEGIN;
SELECT * FROM notification WHERE `date` >= '2011-05-03' FOR UPDATE;

Verbindung 2

BEGIN;

SELECT * FROM notification WHERE `date` = '2011-05-02' FOR UPDATE;

Die gleichzeitige Transaktion 2 wird sicher blockiert, aber der Grund ist NICHT, dass die Transaktion 1 die Sperre für den gesamten Tisch hält. Im Folgenden wird erklärt, was hinter den Kulissen passiert ist:

Zunächst ist die Standardisolationsstufe der InnoDB-Speicher-Engine Repeatable Read. In diesem Fall,

1- Wenn die Spalte, in der die Bedingung verwendet wird, nicht indiziert ist (wie im obigen Fall):

Die Engine muss einen vollständigen Tabellenscan durchführen, um die Datensätze herauszufiltern, die nicht den Kriterien entsprechen. JEDE gescannte REIHE wird an erster Stelle gesperrt. MySQL kann die Sperren für diese Datensätze aufheben, die später nicht mit der where-Klausel übereinstimmen. Es ist eine Optimierung für die Leistung, jedoch verstößt ein solches Verhalten gegen die 2PL-Einschränkung.

Wenn Transaktion 2 wie erläutert gestartet wird, muss sie die X-Sperre für jede abgerufene Zeile abrufen, obwohl nur ein einziger Datensatz (id = 2) vorhanden ist, der mit der where-Klausel übereinstimmt. Schließlich wartet die Transaktion 2 auf die X-Sperre der ersten Zeile (id = 1), bis die Transaktion 1 festgeschrieben oder zurückgesetzt wird.

2- Wenn die Spalte, in der Bedingung verwendet wird, ein Primärindex ist

Nur der Indexeintrag, der die Kriterien erfüllt, ist gesperrt. Deshalb sagt jemand in den Kommentaren, dass einige Tests nicht blockiert sind.

3 - Wenn die Spalte, in der die Bedingung verwendet wird, ein Index ist, der jedoch nicht eindeutig ist

Dieser Fall ist komplizierter. 1) Der Indexeintrag ist gesperrt. 2) Eine X-Sperre ist an den entsprechenden Primärindex angehängt. 3) Unmittelbar vor und nach dem Datensatz, der den Suchkriterien entspricht, werden zwei Lückenschlösser an die nicht vorhandenen Einträge angehängt.

Flyingice
quelle
11

Wenn Sie den Links auf der von Ihnen veröffentlichten Dokumentationsseite folgen, erhalten Sie weitere Informationen zum Sperren . Auf dieser Seite

A SELECT ... FOR UPDATE liest die neuesten verfügbaren Daten und setzt für jede gelesene Zeile exklusive Sperren. Somit werden die gleichen Sperren gesetzt, die ein durchsuchtes SQL-UPDATE für die Zeilen setzen würde.

Dies scheint ziemlich klar zu sein, dass alle Zeilen gescannt werden müssen.

El Yobo
quelle
Sie sind sich nicht sicher, ob ich Sie richtig verstanden habe, aber sagen Sie, dass wenn ich die letzte Zeile auswähle und die Spalten WHEREnicht indiziert sind, die gesamte Tabelle gesperrt wird? Das ist offensichtlich falsch. Zumindest sperrt Oracle nur selected Zeilen
Alexander Malakhov
Oracle ist viel besser als MySQL :) Ich weiß nicht , durch Experimente, nur durch die Dokumentation zu lesen, aber das ist , was es zu sagen scheint. Es klingt allerdings ziemlich dumm.
El Yobo
1
Es klingt enorm dumm. Angesichts der Popularität von MySQL bezweifle ich wirklich, dass es so funktioniert. Und Aussage setting exclusive locks on each row it readskann anders interpretiert werden. Obwohl ich zustimme, ist es nicht 100% klar
Alexander Malakhov
Ich weiß es nicht; MySQL macht einige unglaublich dumme Dinge: - / Ich BEGINNE im Allgemeinen nur, mache alles, was ich tun muss und verpflichte mich, habe aber keine Ahnung, was es intern tut, wenn ich das tue.
El Yobo
3
@ Alexander Malakhov - So scheint es zu funktionieren. Ich habe es gerade getestet und festgestellt, dass es die gesamte Tabelle sperrt, es sei denn, Sie indizieren die Spalten in Ihrer where-Klausel. Die Dokumentation sollte hier wirklich verbessert werden, da sie äußerst verwirrend ist. "Jede Zeile, die es liest" würde mich denken lassen, dass die Zeilen, die es "liest", auf den Bedingungen in meiner WHERE-Klausel basieren würden. Wenn meine WHERE-Klausel das Ergebnis auf 1 zurückgegebene Zeile beschränkt, ist dies die Zeile, von der ich erwarten würde, dass sie gesperrt ist. Aber es scheint, dass "jede Zeile, die es liest" jede Zeile "von der Datenbank gescannt" bedeutet.
DCP
10

Aus dem offiziellen MySQL-Dokument:

Ein Sperrlesevorgang, ein UPDATE oder ein DELETE setzen im Allgemeinen Datensatzsperren für jeden Indexdatensatz, der bei der Verarbeitung der SQL-Anweisung gescannt wird. Es spielt keine Rolle, ob die Anweisung WHERE-Bedingungen enthält, die die Zeile ausschließen würden.

Für den in Frans 'Antwort diskutierten Fall sind alle Zeilen gesperrt, da während der SQL-Verarbeitung ein Tabellenscan durchgeführt wird:

Wenn Sie keine für Ihre Anweisung geeigneten Indizes haben und MySQL die gesamte Tabelle scannen muss, um die Anweisung zu verarbeiten, wird jede Zeile der Tabelle gesperrt, wodurch alle Einfügungen anderer Benutzer in die Tabelle blockiert werden. Es ist wichtig, gute Indizes zu erstellen, damit Ihre Abfragen nicht unnötig viele Zeilen scannen.

Überprüfen Sie das neueste Dokument hier: https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html

yxc
quelle
1

Wie bereits erwähnt, sperrt SELECT ... FOR UPDATE alle Zeilen, die in der Standardisolationsstufe gefunden wurden. Versuchen Sie, die Isolation für die Sitzung, in der diese Abfrage ausgeführt wird, auf READ COMMITTED zu setzen. Stellen Sie der Abfrage beispielsweise Folgendes voraus:set session transaction isolation level read committed;

tcoker
quelle
-4

Es sperrt alle durch Abfrage ausgewählten Zeilen.

r.bhardwaj
quelle
2
Sperrt alle gescannten Zeilen . Obermenge der SELECTed-Zeilen.
Ijoseph