So implementieren Sie optimistisches Sperren korrekt in MySQL

11

Wie implementiert man optimistisches Sperren in MySQL richtig?

Unser Team hat festgestellt, dass wir # 4 unten ausführen müssen, da sonst das Risiko besteht, dass ein anderer Thread dieselbe Version des Datensatzes aktualisieren kann. Wir möchten jedoch bestätigen, dass dies der beste Weg ist, dies zu tun.

  1. Erstellen Sie ein Versionsfeld in der Tabelle, für die Sie eine optimistische Sperre verwenden möchten, z. B. Spaltenname = "Version".
  2. Stellen Sie bei der Auswahl sicher, dass Sie die Versionsspalte einschließen und die Version notieren
  3. Bei einer nachfolgenden Aktualisierung des Datensatzes sollte die Aktualisierungsanweisung "where version = X" ausgeben, wobei X die Version ist, die wir in # 2 erhalten haben, und das Versionsfeld während dieser Aktualisierungsanweisung auf X + 1 setzen
  4. Führen Sie eine SELECT FOR UPDATEAktualisierung des Datensatzes durch, den wir aktualisieren möchten, damit wir serialisieren, wer Änderungen an dem Datensatz vornehmen kann, den wir aktualisieren möchten.

Zur Verdeutlichung versuchen wir zu verhindern, dass zwei Threads, die denselben Datensatz im selben Zeitfenster auswählen, in dem sie dieselbe Version des Datensatzes abrufen, sich gegenseitig überschreiben, wenn sie versuchen, den Datensatz gleichzeitig zu aktualisieren. Wir glauben, dass, wenn wir nicht # 4 tun, die Möglichkeit besteht, dass, wenn beide Threads ihre jeweiligen Transaktionen gleichzeitig eingeben (aber ihre Updates noch nicht veröffentlicht haben), wenn sie zur Aktualisierung gehen, der zweite Thread, der das UPDATE verwendet ... wobei version = X mit alten Daten arbeitet.

Denken wir zu Recht, dass wir diese pessimistische Sperrung beim Aktualisieren durchführen müssen, obwohl wir Versionsfelder / optimistische Sperrung verwenden?

Empfohlene Vorgehensweise
quelle
Was ist das Problem? Wenn Sie die Versionsnummer mit Ihrem UPDATE erhöhen, schlägt das zweite UPDATE fehl, da die Versionsnummer nicht mit der beim Lesen übereinstimmt - was Sie möchten.
AndreKR
Bist du sicher? Es ist unklar, dass die anderen Threads tatsächlich aktualisiert werden, wenn Sie die Transaktionsisolationsstufe nicht auf eine bestimmte Einstellung festlegen. Wenn Sie beide gleichzeitig die Transaktion eingeben, kann der zweite Thread die ALTEN Daten sehr gut sehen, wenn er das Update durchführt. MySQL ist im ACID-Bereich nicht so robust wie beispielsweise Oracle und sucht daher nach bewährten Methoden, um optimistisches Sperren in MySQL zu implementieren, das fehlerhafte Lese- / Aktualisierungen verhindert.
BestPractices
Aber dann schlägt die Transaktion beim Festschreiben trotzdem fehl, oder?
AndreKR
Anzeichen dafür sind, dass man eine Auswahl für ein Update treffen möchte, um mit dieser Situation fertig zu werden
BestPractices
@BestPractices Sie benötigen entweder eine SELECT ... FOR UPDATE oder eine optimistische Sperre durch Zeilenversionierung, nicht beide. Siehe Detail in der Antwort.
Craig Ringer

Antworten:

15

Ihr Entwickler irrt sich. Sie benötigen eine SELECT ... FOR UPDATE oder eine Zeilenversionierung, nicht beide.

Probieren Sie es aus und sehen Sie. Öffnen drei MySQL - Sitzungen (A), (B)und(C) auf die gleiche Datenbank.

In (C)Ausgabe:

CREATE TABLE test(
    id integer PRIMARY KEY,
    data varchar(255) not null,
    version integer not null
);
INSERT INTO test(id,data,version) VALUES (1,'fred',0);
BEGIN;
LOCK TABLES test WRITE;

In beiden (A)und in (B)einem UPDATE, der die Zeilenversion testet und festlegt, ändern Sie den winnerText in jeder, damit Sie sehen können, welche Sitzung welche ist:

-- In (A):

BEGIN;
UPDATE test SET data = 'winnerA',
            version = version + 1
WHERE id = 1 AND version = 0;

-- in (B):

BEGIN;
UPDATE test SET data = 'winnerB',
            version = version + 1
WHERE id = 1 AND version = 0;

Jetzt in (C), UNLOCK TABLES;um das Schloss zu lösen.

(A)und (B)wird um die Zeilensperre rennen. Einer von ihnen wird gewinnen und das Schloss bekommen. Der andere blockiert das Schloss. Der Gewinner, der die Sperre erhalten hat, wechselt die Zeile. Angenommen, (A)der Gewinner ist, können Sie jetzt die geänderte Zeile (immer noch nicht festgeschrieben, also für andere Transaktionen nicht sichtbar) mit a sehenSELECT * FROM test WHERE id = 1 .

COMMITSagen wir jetzt in der Gewinnersitzung (A).

(B)wird die Sperre erhalten und mit dem Update fortfahren. Die Version stimmt jedoch nicht mehr überein, sodass keine Zeilen geändert werden, wie aus dem Ergebnis der Zeilenanzahl hervorgeht. Nur einer UPDATEhatte einen Effekt, und die Client-Anwendung kann klar erkennen, welcher UPDATEerfolgreich war und welcher fehlgeschlagen ist. Es ist keine weitere Verriegelung erforderlich.

Siehe Sitzungsprotokolle bei Pastebin hier . Ich habe mysql --prompt="A> "usw. verwendet, um es einfach zu machen, den Unterschied zwischen Sitzungen zu erkennen. Ich habe die Ausgabe in zeitlicher Reihenfolge kopiert und eingefügt, daher handelt es sich nicht um eine vollständige Rohausgabe, und es ist möglich, dass ich beim Kopieren und Einfügen Fehler gemacht habe. Testen Sie es selbst, um zu sehen.


Wenn Sie hatte nicht eine Zeile Versionsfeld hinzugefügt, dann müssten SieSELECT ... FOR UPDATE der Lage sein, zuverlässig Ordnung zu gewährleisten.

Wenn Sie darüber nachdenken, SELECT ... FOR UPDATEist a völlig redundant, wenn Sie sofort eine UPDATEDaten ohne Wiederverwendung von Daten aus dem SELECToder ohne Zeilenversionierung ausführen. Der UPDATEwird sowieso eine Sperre nehmen. Wenn jemand anderes die Zeile zwischen Ihrem Lesen und dem anschließenden Schreiben aktualisiert, stimmt Ihre Version nicht mehr überein, sodass Ihr Update fehlschlägt. So funktioniert optimistisches Sperren.

Der Zweck von SELECT ... FOR UPDATEist:

  • Verwalten der Sperrenreihenfolge, um Deadlocks zu vermeiden; und
  • Um die Spanne einer Zeilensperre zu verlängern, wenn Sie Daten aus einer Zeile lesen möchten, ändern Sie sie in der Anwendung und schreiben Sie eine neue Zeile, die auf der ursprünglichen basiert, ohne sie verwenden zu müssen SERIALIZABLE Isolation oder Zeilenversionierung verwenden zu müssen.

Sie müssen nicht sowohl optimistisches Sperren (Zeilenversionierung) als auch verwenden SELECT ... FOR UPDATE. Verwenden Sie den einen oder anderen.

Craig Ringer
quelle
Danke Craig. Sie hatten Recht - der Entwickler hat sich geirrt. Vielen Dank, dass Sie diesen Test durchgeführt haben.
BestPractices
Was ist mit SQL Server? Gibt es immer eine Sperre für die aktualisierte Zeile, unabhängig von der Transaktionsisolationsstufe?
Plalx
@plalx Nun, was sagt die Dokumentation? Was passiert, wenn Sie einen interaktiven Test wie diesen durchführen?
Craig Ringer
@CraigRinger, was passiert, wenn B die Sperre vor A-Commit, aber nach A-Update erhält?
MengT
1
@MengT Es kann nicht, deshalb ist es ein Schloss.
Craig Ringer
0
UPDATE tbl SET owner = $me,
               id = LAST_INSERT_ID(id)
    WHERE owner = ''
    LIMIT 1;
$id = SELECT LAST_INSERT_ID();
Do some stuff (arbitrarily long time)...;
UPDATE  tbl SET owner = '' WHERE id = $id;

Keine Sperren (keine Tabelle, keine Transaktion) erforderlich oder sogar erwünscht:

  • UPDATE ist atomar
  • LAST_INSERT_ID () ist sitzungsspezifisch und daher threadsicher.
Rick James
quelle