So vermeiden Sie mysql 'Deadlock beim Versuch, eine Sperre zu erhalten; Versuchen Sie, die Transaktion neu zu starten. '

286

Ich habe eine innoDB-Tabelle, die Online-Benutzer aufzeichnet. Es wird bei jeder Seitenaktualisierung durch einen Benutzer aktualisiert, um zu verfolgen, auf welchen Seiten er sich befindet und wann er zuletzt auf die Site zugegriffen hat. Ich habe dann einen Cron, der alle 15 Minuten läuft, um alte Datensätze zu löschen.

Ich habe einen Deadlock gefunden, als ich versucht habe, einen Lock zu bekommen. Versuchen Sie, die Transaktion 'letzte Nacht etwa 5 Minuten lang neu zu starten. Dies scheint der Fall zu sein, wenn INSERTs in dieser Tabelle ausgeführt werden. Kann jemand vorschlagen, wie dieser Fehler vermieden werden kann?

=== BEARBEITEN ===

Hier sind die Abfragen, die ausgeführt werden:

Erster Besuch auf der Website:

INSERT INTO onlineusers SET
ip = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3

Auf jeder Seite aktualisieren:

UPDATE onlineusers SET
ips = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3
WHERE id = 888

Cron alle 15 Minuten:

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Es zählt dann einige Statistiken, um einige Statistiken zu protokollieren (z. B. Mitglieder online, Besucher online).

David
quelle
Können Sie weitere Details zur Tabellenstruktur angeben? Gibt es Clustered- oder Nonclustered-Indizes?
Anders Abel
13
dev.mysql.com/doc/refman/5.1/de/innodb-deadlocks.html - Das Ausführen von "show engine innodb status" bietet nützliche Diagnosen.
Martin
Es ist keine gute Praxis, einen synchronen Datenbankschreibvorgang durchzuführen, wenn Benutzer auf eine Seite treten. Der richtige Weg, dies zu tun, besteht darin, es im Speicher wie Memcache oder einer schnellen Warteschlange zu speichern und mit Cron in die Datenbank zu schreiben.
Nir

Antworten:

292

Ein einfacher Trick, der bei den meisten Deadlocks helfen kann, besteht darin, die Vorgänge in einer bestimmten Reihenfolge zu sortieren.

Sie erhalten einen Deadlock, wenn zwei Transaktionen versuchen, zwei Sperren in entgegengesetzter Reihenfolge zu sperren, dh:

  • Verbindung 1: sperrt Schlüssel (1), sperrt Schlüssel (2);
  • Verbindung 2: sperrt Schlüssel (2), sperrt Schlüssel (1);

Wenn beide gleichzeitig ausgeführt werden, sperrt Verbindung 1 den Schlüssel (1), Verbindung 2 den Schlüssel (2) und jede Verbindung wartet darauf, dass die andere den Schlüssel freigibt -> Deadlock.

Wenn Sie nun Ihre Abfragen so geändert haben, dass die Verbindungen die Schlüssel in derselben Reihenfolge sperren, dh:

  • Verbindung 1: sperrt Schlüssel (1), sperrt Schlüssel (2);
  • Verbindung 2: sperrt Schlüssel ( 1 ), sperrt Schlüssel ( 2 );

Es wird unmöglich sein, einen Deadlock zu bekommen.

Das schlage ich also vor:

  1. Stellen Sie sicher, dass Sie außer der delete-Anweisung keine anderen Abfragen haben, die den Zugriff auf mehr als einen Schlüssel gleichzeitig sperren. Wenn Sie dies tun (und ich vermute, dass Sie dies tun), ordnen Sie deren WO in (k1, k2, .. kn) in aufsteigender Reihenfolge an.

  2. Korrigieren Sie Ihre Löschanweisung so, dass sie in aufsteigender Reihenfolge funktioniert:

Veränderung

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Zu

DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers
    WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u;

Eine andere Sache, die Sie beachten sollten, ist, dass die MySQL-Dokumentation vorschlägt, dass der Client im Falle eines Deadlocks automatisch erneut versuchen sollte. Sie können diese Logik zu Ihrem Client-Code hinzufügen. (Sagen wir, 3 Versuche zu diesem bestimmten Fehler, bevor Sie aufgeben).

Omry Yadan
quelle
2
Wenn ich eine Transaktion habe (autocommit = false), wird eine Deadlock-Ausnahme ausgelöst. Reicht es aus, nur dieselbe Anweisung erneut zu versuchen.executeUpdate () oder die gesamte Transaktion ist jetzt gimped und sollte zurückgesetzt werden + alles, was darin ausgeführt wurde, erneut ausführen?
Wen
5
Wenn Sie Transaktionen aktiviert haben, ist alles oder nichts. Wenn Sie eine Ausnahme hatten, ist garantiert, dass die gesamte Transaktion keine Auswirkungen hatte. In diesem Fall möchten Sie das Ganze neu starten.
Omry Yadan
4
Ein Löschen basierend auf einer Auswahl auf einer großen Tabelle ist sehr langsamer als ein einfaches Löschen
Thermech
3
Vielen Dank, Alter. Der Tipp "Sortieranweisungen" hat meine Deadlock-Probleme behoben.
Miere
4
@OmryYadan Soweit ich weiß, können Sie in MySQL keine Unterabfrage aus derselben Tabelle auswählen, in der Sie das UPDATE erstellen. dev.mysql.com/doc/refman/5.7/en/update.html
artaxerxe
72

Ein Deadlock tritt auf, wenn zwei Transaktionen aufeinander warten, um eine Sperre zu erhalten. Beispiel:

  • Tx 1: Sperre A, dann B.
  • Tx 2: Sperre B, dann A.

Es gibt zahlreiche Fragen und Antworten zu Deadlocks. Jedes Mal, wenn Sie eine Zeile einfügen / aktualisieren / oder löschen, wird eine Sperre erworben. Um einen Deadlock zu vermeiden, müssen Sie sicherstellen, dass gleichzeitige Transaktionen die Zeile nicht in einer Reihenfolge aktualisieren, die zu einem Deadlock führen kann. Versuchen Sie im Allgemeinen, die Sperre auch in verschiedenen Transaktionen immer in derselben Reihenfolge zu erhalten (z. B. immer zuerst Tabelle A, dann Tabelle B).

Ein weiterer Grund für einen Deadlock in der Datenbank können fehlende Indizes sein . Wenn eine Zeile eingefügt / aktualisiert / gelöscht wird, muss die Datenbank die relationalen Einschränkungen überprüfen, dh sicherstellen, dass die Beziehungen konsistent sind. Dazu muss die Datenbank die Fremdschlüssel in den zugehörigen Tabellen überprüfen. Es könnte in anderer Schloss ist als die Zeile erworben führen , die geändert wird. Stellen Sie dann sicher, dass die Fremdschlüssel (und natürlich die Primärschlüssel) immer indexiert sind, da dies sonst zu einer Tabellensperre anstelle einer Zeilensperre führen kann . Wenn eine Tabellensperre auftritt, ist der Sperrenkonflikt höher und die Wahrscheinlichkeit eines Deadlocks steigt.

ewernli
quelle
3
Vielleicht besteht mein Problem darin, dass der Benutzer die Seite aktualisiert hat und somit ein UPDATE eines Datensatzes auslöst, während der Cron versucht, ein DELETE für den Datensatz auszuführen. Ich erhalte jedoch den Fehler bei INSERTS, sodass der Cron keine gerade erstellten Datensätze löscht. Wie kann es also zu einem Deadlock bei einem Datensatz kommen, der noch eingefügt werden muss?
David
Können Sie etwas mehr Informationen über die Tabelle (n) und die genaue Funktionsweise der Transaktionen bereitstellen?
Ewernli
Ich sehe nicht ein, wie ein Deadlock passieren könnte, wenn es nur eine Anweisung pro Transaktion gibt. Keine anderen Operationen an anderen Tabellen? Keine speziellen Fremdschlüssel oder eindeutigen Einschränkungen? Keine Kaskadenlöschbeschränkungen?
Ewernli
Nein, nichts Besonderes ... Ich nehme an, es liegt an der Art der Verwendung der Tabelle. Bei jeder Seitenaktualisierung eines Besuchers wird eine Zeile eingefügt / aktualisiert. Über 1000 Besucher sind gleichzeitig anwesend.
David
12

Es ist wahrscheinlich, dass die delete-Anweisung einen großen Teil der gesamten Zeilen in der Tabelle beeinflusst. Dies kann möglicherweise dazu führen, dass beim Löschen eine Tabellensperre aktiviert wird. Das Festhalten an einer Sperre (in diesem Fall Zeilen- oder Seitensperren) und der Erwerb weiterer Sperren ist immer ein Deadlock-Risiko. Ich kann jedoch nicht erklären, warum die insert-Anweisung zu einer Eskalation der Sperren führt - dies hat möglicherweise mit dem Teilen / Hinzufügen von Seiten zu tun, aber jemand, der MySQL besser kennt, muss sie dort ausfüllen.

Zunächst kann es sich lohnen, sofort explizit eine Tabellensperre für die delete-Anweisung zu erwerben. Siehe Probleme mit LOCK TABLES und Table Locking .

Anders Abel
quelle
6

Sie können versuchen, diesen deleteJob auszuführen, indem Sie zuerst den Schlüssel jeder zu löschenden Zeile in eine temporäre Tabelle wie diesen Pseudocode einfügen

create temporary table deletetemp (userid int);

insert into deletetemp (userid)
  select userid from onlineusers where datetime <= now - interval 900 second;

delete from onlineusers where userid in (select userid from deletetemp);

Eine solche Aufteilung ist weniger effizient, vermeidet jedoch die Notwendigkeit, während des Vorgangs ein Schlüsselbereichsschloss zu halten delete.

Ändern Sie außerdem Ihre selectAbfragen, um eine whereKlausel hinzuzufügen , die Zeilen ausschließt, die älter als 900 Sekunden sind. Dies vermeidet die Abhängigkeit vom Cron-Job und ermöglicht es Ihnen, ihn neu zu planen, damit er seltener ausgeführt wird.

Theorie über die Deadlocks: Ich habe nicht viel Hintergrundwissen in MySQL, aber hier deleteist es ... Das wird eine Schlüsselbereichssperre für datetime halten, um zu verhindern, dass Zeilen, die mit seiner whereKlausel übereinstimmen, während der Transaktion hinzugefügt werden Wenn Zeilen zum Löschen gefunden werden, wird versucht, auf jeder Seite, die geändert wird, eine Sperre zu erhalten. Der insertwird eine Sperre für die Seite erhalten, in die er eingefügt wird, und dann versuchen, die Schlüsselsperre zu erhalten. Normalerweise insertwartet der geduldig darauf, dass sich diese Tastensperre öffnet, aber dies blockiert, wenn deleteversucht wird, dieselbe Seite zu sperren, die er insertverwendet, weil er deletediese Seitensperre und die insertTastensperre benötigt. Dies scheint jedoch nicht richtig für Einfügungen zu sein, die deleteundinsert verwenden Datums- / Uhrzeitbereiche, die sich nicht überlappen, sodass möglicherweise etwas anderes passiert.

http://dev.mysql.com/doc/refman/5.1/en/innodb-next-key-locking.html

Brian Sandlin
quelle
4

Falls noch jemand mit diesem Problem zu kämpfen hat:

Ich hatte ein ähnliches Problem, bei dem zwei Anfragen gleichzeitig auf den Server trafen. Es gab keine Situation wie unten:

T1:
    BEGIN TRANSACTION
    INSERT TABLE A
    INSERT TABLE B
    END TRANSACTION

T2:
    BEGIN TRANSACTION
    INSERT TABLE B
    INSERT TABLE A
    END TRANSACTION

Also war ich verwirrt, warum ein Deadlock passiert.

Dann stellte ich fest, dass zwischen zwei Tabellen aufgrund eines Fremdschlüssels eine Eltern-Kind-Beziehung bestand. Als ich einen Datensatz in eine untergeordnete Tabelle einfügte, erwarb die Transaktion eine Sperre für die Zeile der übergeordneten Tabelle. Unmittelbar danach habe ich versucht, die übergeordnete Zeile zu aktualisieren, die die Erhöhung der Sperre auf EXKLUSIV auslöste. Da die zweite gleichzeitige Transaktion bereits eine SHARED-Sperre enthielt, verursachte sie einen Deadlock.

Siehe: https://blog.tekenlight.com/2019/02/21/database-deadlock-mysql.html

chatsap
quelle
Auch in meinem Fall scheint das Problem eine Fremdschlüsselbeziehung zu sein. Thanks1
Chris Prince
3

Für Java-Programmierer, die Spring verwenden, habe ich dieses Problem vermieden, indem ich einen AOP-Aspekt verwendet habe, der automatisch Transaktionen wiederholt, die auf vorübergehende Deadlocks stoßen.

Weitere Informationen finden Sie unter @RetryTransaction Javadoc.

Archie
quelle
0

Ich habe eine Methode, deren Interna in eine MySqlTransaction eingeschlossen sind.

Das Deadlock-Problem trat für mich auf, als ich dieselbe Methode parallel zu sich selbst ausführte.

Es gab kein Problem beim Ausführen einer einzelnen Instanz der Methode.

Als ich MySqlTransaction entfernte, konnte ich die Methode ohne Probleme parallel zu sich selbst ausführen.

Ich teile nur meine Erfahrungen und befürworte nichts.

CINCHAPPS
quelle
0

cronist gefährlich. Wenn eine Instanz von Cron nicht fertig ist, bevor die nächste fällig ist, kämpfen sie wahrscheinlich gegeneinander.

Es wäre besser, einen kontinuierlich laufenden Job zu haben, der einige Zeilen löscht, einige in den Ruhezustand versetzt und dann wiederholt.

Auch INDEX(datetime)ist sehr wichtig, um Deadlocks zu vermeiden.

Wenn der Datetime-Test jedoch mehr als beispielsweise 20% der Tabelle enthält, DELETEführt er einen Tabellenscan durch. Kleinere Blöcke, die häufiger gelöscht werden, sind eine Problemumgehung.

Ein weiterer Grund für kleinere Blöcke besteht darin, weniger Zeilen zu sperren.

Endeffekt:

  • INDEX(datetime)
  • Kontinuierlich ausgeführte Aufgabe - löschen, eine Minute schlafen, wiederholen.
  • Um sicherzustellen, dass die oben genannte Aufgabe nicht gestorben ist, haben Sie einen Cron-Job, dessen einziger Zweck darin besteht, sie bei einem Fehler neu zu starten.

Andere Löschtechniken: http://mysql.rjweb.org/doc.php/deletebig

Rick James
quelle