MySQL Deadlocks auflösen: Strategien und Best Practices

MySQL-Deadlocks diagnostizieren, Sperrkonflikte reduzieren und sichere Wiederholungslogik für wiederholbare Transaktionen hinzufügen.

MySQL Deadlocks auflösen: Strategien und Best Practices

MySQL-Deadlocks treten auf, wenn Transaktionen sich gegenseitig in einem Kreislauf blockieren. InnoDB erkennt den Kreislauf und rollt eine Transaktion zurück, damit die andere fortfahren kann. Dieses automatische Rollback ist hilfreich, macht das Problem jedoch nicht harmlos. Die Anwendung sieht immer noch einen Fehler, die Benutzeraktion kann fehlschlagen, und wiederholte Deadlocks können einen normalen Verkehrsspitze in einen Support-Vorfall verwandeln.

Ein einzelner Deadlock hin und wieder ist in einem geschäftigen Transaktionssystem nicht ungewöhnlich. Ein stetiger Strom von Deadlocks ist ein Signal. Normalerweise bedeutet dies, dass Transaktionen zu breit sind, Zeilen in inkonsistenter Reihenfolge gesperrt werden oder Abfragen mehr Daten scannen als nötig.

MySQL-Deadlocks verstehen

In der modernen MySQL-Arbeit bedeutet die Fehlersuche bei Deadlocks normalerweise InnoDB, da es die standardmäßige Transaktions-Engine ist und Zeilen-, Index-, Lücken- und Next-Key-Sperren verwendet. Andere Engines können blockieren, Zeitüberschreitungen verursachen oder Schreibvorgänge anders serialisieren, aber InnoDB ist der Ort, an dem die meisten Deadlock-Vorfälle in Anwendungen auftreten.

Der Deadlock-Kreislauf

Ein Deadlock folgt typischerweise diesem Muster:

  1. Transaktion A erwirbt eine Sperre auf Ressource X.
  2. Transaktion B erwirbt eine Sperre auf Ressource Y.
  3. Transaktion A versucht, eine Sperre auf Ressource Y zu erwerben, muss aber warten, weil B sie hält.
  4. Transaktion B versucht, eine Sperre auf Ressource X zu erwerben, muss aber warten, weil A sie hält.

An diesem Punkt kann keine Transaktion fortfahren. InnoDB erkennt diesen Wartekreislauf und rollt eine Transaktion zurück, die oft als Deadlock-Opfer bezeichnet wird. Die Anwendung erhält normalerweise den SQL-Fehlercode 1213 (ER_LOCK_DEADLOCK). Die gesamte Transaktion ist verloren, nicht nur die letzte Anweisung, daher muss die Wiederholungslogik die Transaktion von Anfang an erneut ausführen.

Häufige Ursachen von Deadlocks

Deadlocks entstehen in der Regel aus schlechtem Transaktionsdesign oder ineffizienten Abfragen:

  • Langlaufende Transaktionen: Transaktionen, die Sperren über längere Zeiträume halten, erhöhen die Wahrscheinlichkeit von Kollisionen drastisch.
  • Inkonsistente Operationsreihenfolge: Zwei Transaktionen, die denselben Satz von Zeilen oder Tabellen aktualisieren, jedoch in einer unterschiedlichen Reihenfolge.
  • Fehlende oder ineffiziente Indizes: Wenn Indizes fehlen oder nicht selektiv genug sind, kann InnoDB weit mehr Indexdatensätze oder -bereiche untersuchen und sperren als von der Anwendung erwartet, was die Sperroberfläche vergrößert.
  • Hohe Parallelität: Natürlich erhöhen schwere gleichzeitige Schreibvorgänge auf dieselben Datensätze die Kollisionswahrscheinlichkeit.

Diagnose und Analyse von Deadlocks

Wenn ein Deadlock auftritt, besteht der erste Schritt darin, die beteiligten Transaktionen und die spezifischen Sperren zu identifizieren, die sie gehalten haben. Das primäre Diagnosetool in MySQL ist SHOW ENGINE INNODB STATUS.

Verwenden von SHOW ENGINE INNODB STATUS

Führen Sie den folgenden Befehl aus und untersuchen Sie die Ausgabe, insbesondere den Abschnitt LATEST DETECTED DEADLOCK.

SHOW ENGINE INNODB STATUS\G

Die Ausgabe von LATEST DETECTED DEADLOCK liefert entscheidende forensische Daten, die Folgendes detaillieren:

  1. Die beteiligten Transaktionen (ID, Status und Dauer).
  2. Die SQL-Anweisung, die das Opfer ausführte, als der Deadlock auftrat.
  3. Die spezifische Zeile und der Index, auf die gewartet wurde.
  4. Die Ressourcen, die von der blockierenden Transaktion gehalten wurden.

Tipp: Log-Parsing-Tools können diese Deadlock-Einträge automatisch extrahieren und kategorisieren, die auch oft in das MySQL-Fehlerprotokoll geschrieben werden.

Erfassen Sie bei wiederkehrenden Vorfällen mehr als eine Probe. SHOW ENGINE INNODB STATUS zeigt den zuletzt erkannten Deadlock an, sodass ein neuer Deadlock die vorherigen Beweise überschreiben kann. Aktivieren Sie auf Systemen mit häufigen Deadlocks die Deadlock-Protokollierung im Fehlerprotokoll, wenn Ihre MySQL-Version und Betriebsrichtlinie dies zulassen:

SET GLOBAL innodb_print_all_deadlocks = ON;

Lassen Sie die zusätzliche Protokollierung nicht ohne Berücksichtigung des Volumens aktiviert. Sie ist während eines Vorfalls nützlich, aber laute Systeme können viele Protokolldaten produzieren.

Wenn Sie einen Deadlock-Bericht lesen, suchen Sie vor dem Ändern von Code nach drei Dingen:

  1. Welche SQL-Anweisungen waren beteiligt?
  2. Welche Indizes wurden verwendet?
  3. Haben die Transaktionen dieselben Ressourcen in unterschiedlicher Reihenfolge gesperrt?

Die letzte Frage ist normalerweise die aufschlussreichste. Wenn ein Codepfad accounts und dann ledger_entries aktualisiert, während ein anderer ledger_entries und dann accounts aktualisiert, liegt die Lösung wahrscheinlich nicht in einer Servereinstellung. Die Lösung besteht darin, sich auf eine Reihenfolge zu einigen.

Ein praktischer Trick besteht darin, die beiden Transaktionen in eine Notiz zu kopieren und jede Anweisung zu markieren, die Zeilen sperren kann: UPDATE, DELETE, INSERT in Tabellen mit eindeutigen Schlüsseln, SELECT ... FOR UPDATE und einige Fremdschlüsselprüfungen. Deadlocks werden oft offensichtlich, wenn Sie nicht mehr auf die einzelne fehlgeschlagene Abfrage schauen, sondern auf die gesamte Sequenz darum herum.

Präventionsstrategie 1: Optimierung von Transaktionen

Der effektivste Weg, Deadlocks zu verhindern, besteht darin, die Zeit zu reduzieren, in der Sperren gehalten werden, und den Zugriff auf Ressourcen zu standardisieren.

1. Halten Sie Transaktionen kurz und atomar

Eine Transaktion sollte nur die unbedingt notwendigen Operationen umfassen. Je länger eine Transaktion läuft, desto länger hält sie Sperren und desto höher ist die Wahrscheinlichkeit einer Kollision.

  • Schlechte Praxis: Daten abrufen, komplexe Geschäftslogik in der Anwendungsschicht ausführen und dann Daten aktualisieren, alles innerhalb einer langen Transaktion.
  • Beste Praxis: Führen Sie die Geschäftslogik außerhalb der Transaktion aus. Die Transaktion sollte nur die Schritte SELECT FOR UPDATE, Update/Insert und COMMIT umfassen.

Öffnen Sie beispielsweise keine Transaktion und rufen Sie dann einen Zahlungsanbieter auf, senden Sie eine E-Mail oder warten Sie auf eine entfernte API. Diese Operationen können Hunderte von Millisekunden oder Sekunden dauern, und während dieser Zeit werden Ihre Datenbanksperren weiterhin gehalten. Berechnen Sie zuerst, was Sie können, und öffnen Sie dann die Transaktion für die kleinste Menge an Lese- und Schreibvorgängen, die atomar sein müssen.

2. Standardisieren Sie die Reihenfolge des Ressourcenzugriffs

Dies ist vielleicht die wichtigste Präventionsstrategie überhaupt. Wenn jeder Code, der mit zwei bestimmten Tabellen interagiert (z. B. orders und inventory), immer versucht, die Tabellen (oder Zeilen) in derselben Reihenfolge zu sperren (z. B. orders dann inventory), werden zirkuläre Abhängigkeiten unmöglich.

Transaktion A Transaktion B
Sperre Tabelle X Sperre Tabelle Y
Sperre Tabelle Y Sperre Tabelle X (DEADLOCK-RISIKO)

Wenn beide Transaktionen der Sequenz (X dann Y) folgen würden, würde Transaktion B einfach warten, bis A fertig ist, wodurch der Deadlock verhindert würde.

Dies gilt auch für Zeilen. Wenn eine Überweisung Geld zwischen zwei Konten verschiebt, sperren Sie immer zuerst die niedrigere Konto-ID und dann die höhere Konto-ID:

START TRANSACTION;

SELECT account_id, balance
FROM accounts
WHERE account_id IN (100, 200)
ORDER BY account_id
FOR UPDATE;

-- Belastung und Gutschrift anwenden

COMMIT;

Ohne eine konsistente Reihenfolge können zwei gleichzeitige Überweisungen in entgegengesetzte Richtungen einen Deadlock verursachen: Eine sperrt Konto 100, während die andere Konto 200 sperrt, und dann wartet jede auf die andere Zeile.

3. Verwenden Sie SELECT FOR UPDATE strategisch

Wenn Sie Daten lesen, die später in derselben Transaktion sofort geändert werden, verwenden Sie SELECT FOR UPDATE, um sofort eine exklusive Sperre zu erwerben. Dies verhindert, dass eine zweite Transaktion dieselbe Zeile zwischen Ihrem Lese- und Schreibvorgang ändert. Es entfernt Deadlocks nicht auf magische Weise; es macht die Sperrung explizit, sodass Sie die Reihenfolge und das Timing steuern können.

-- Sofortige Sperre auf der/den angegebenen Zeile(n) erwerben
SELECT amount FROM accounts WHERE user_id = 123 FOR UPDATE;
-- Berechnungen in der Anwendung durchführen
UPDATE accounts SET amount = new_amount WHERE user_id = 123;
COMMIT;

Präventionsstrategie 2: Indizierung und Abfrageoptimierung

Schlechte Indizierung ist eine häufige Grundursache, da sie InnoDB dazu zwingt, mehr Zeilen als nötig zu sperren.

1. Stellen Sie sicher, dass Abfragen Indizes für die Sperrung verwenden

Wenn MySQL Zeilen basierend auf einer WHERE-Klausel lokalisieren muss, sperrt es die Indexdatensätze, die der Bedingung entsprechen. Wenn kein geeigneter Index vorhanden ist, kann InnoDB einen vollständigen Tabellenscan durchführen und die gesamte Tabelle (oder große Bereiche) sperren, selbst wenn nur wenige Zeilen benötigt werden.

  • Stellen Sie sicher, dass Spalten, die zum Auffinden von Zeilen für Updates, Löschungen, Joins und Bereichsscans verwendet werden, über geeignete Indizes verfügen.
  • Stellen Sie sicher, dass Fremdschlüssel indiziert sind.

Verwenden Sie EXPLAIN für die im Deadlock-Bericht gezeigten Anweisungen:

EXPLAIN UPDATE orders
SET status = 'paid'
WHERE user_id = 42 AND status = 'pending';

Wenn MySQL einen großen Teil einer Tabelle scannt, um eine Handvoll Zeilen zu aktualisieren, kann es Sperren halten, die Sie nicht erwartet haben. Ein zusammengesetzter Index wie (user_id, status) kann den Scan eingrenzen. Der richtige Index hängt von Ihren tatsächlichen Abfragemustern und Ihrer Datenverteilung ab. Bestätigen Sie dies daher mit EXPLAIN und produktionsähnlichen Daten.

2. Minimieren Sie Lückensperren

InnoDB verwendet Lückensperren (Sperren auf Bereichen zwischen Indexdatensätzen) in der standardmäßigen Isolationsstufe REPEATABLE READ, um Phantom-Lesevorgänge zu verhindern. Obwohl sie für die Konsistenz unerlässlich sind, sind diese Sperren oft für Deadlocks verantwortlich, wenn sich Bereiche überschneiden.

Wenn Sie mit Schreibvorgängen mit hoher Parallelität umgehen und ein anderes Leseverhalten tolerieren können, sollten Sie in Betracht ziehen, die Isolationsstufe für bestimmte Sitzungen auf READ COMMITTED umzustellen.

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

Warnung: Das globale oder unvorsichtige Ändern der Isolationsstufe kann ein Verhalten einführen, das Ihre Anwendung nicht erwartet hat, wie z. B. nicht wiederholbare Lesevorgänge. Verwenden Sie READ COMMITTED nur dort, wo die Risiken verstanden und getestet wurden.

Lösungsstrategie: Wiederholungslogik auf Anwendungsseite

Selbst mit den besten Präventionsstrategien können Deadlocks unter extremer Last gelegentlich auftreten. Da InnoDB das Opfer automatisch zurückrollt, muss die Anwendung so ausgelegt sein, dass sie diesen Fehler ordnungsgemäß behandelt.

MySQL meldet einen Deadlock mit dem SQL-Fehlercode 1213 (ER_LOCK_DEADLOCK).

Implementieren einer Transaktionswiederholung

Anwendungen sollten Fehler 1213 abfangen und die gesamte Transaktion wiederholen, wenn der Vorgang sicher wiederholt werden kann. Eine Wiederholung muss mit START TRANSACTION beginnen; führen Sie nicht nur die fehlgeschlagene Anweisung erneut aus.

  1. Fehler 1213 abfangen: Der Datenbank-Connector sollte den Deadlock-Fehler erkennen.
  2. Warten: Führen Sie eine kurze, zufällige Backoff-Zeit ein, bevor Sie es erneut versuchen, damit die blockierende Transaktion Zeit zum Commit hat.
  3. Wiederholen: Versuchen Sie die vollständige Transaktionssequenz erneut.
  4. Wiederholungen begrenzen: Implementieren Sie eine maximale Anzahl von Wiederholungen (z. B. 3 bis 5), bevor die Benutzeranforderung fehlschlägt, um Endlosschleifen zu vermeiden.
MAX_RETRIES = 5

for attempt in range(MAX_RETRIES):
    try:
        db_connection.execute("START TRANSACTION")
        # ... komplexe Datenbankoperationen ...
        db_connection.execute("COMMIT")
        break # Erfolg
    except DeadlockError:
        if attempt < MAX_RETRIES - 1:
            time.sleep(0.1 * (attempt + 1)) # Exponentielles Backoff
            continue
        else:
            raise DatabaseFailure("Transaktion aufgrund anhaltenden Deadlocks fehlgeschlagen.")

Seien Sie vorsichtig mit Transaktionen, die externe Effekte auslösen. Wenn die Transaktion eine E-Mail sendet, eine Karte belastet, eine Nachricht veröffentlicht oder einen anderen Dienst aufruft, kann eine blinde Wiederholung diesen Nebeneffekt duplizieren. Verwenden Sie in diesen Fällen Idempotenzschlüssel oder verschieben Sie den Nebeneffekt nach dem Datenbank-Transaktions-Commit.

Erweiterte Einstellungen und Best Practices

Anpassen des Sperrwartezeitlimits

MySQL hat eine Einstellung, die definiert, wie lange eine Transaktion auf eine Sperre warten soll, bevor sie aufgibt:

SET GLOBAL innodb_lock_wait_timeout = 50; -- Bis zu 50 Sekunden warten

Ein zu niedrig eingestelltes innodb_lock_wait_timeout kann dazu führen, dass Transaktionen bei normaler Konkurrenz fehlschlagen. Ein zu hoher Wert kann dazu führen, dass Benutzeranfragen länger warten, als Ihre Anwendung tolerieren kann. Diese Einstellung behandelt Sperrwartezeiten, nicht die Deadlock-Erkennung selbst. Behandeln Sie sie daher nicht als primäre Deadlock-Lösung.

Zusammenfassung der Best Practices

Bereich Best Practice
Transaktionsdesign Halten Sie Transaktionen kurz, führen Sie sie schnell aus und committen oder rollen Sie sie sofort zurück.
Sperrreihenfolge Legen Sie eine strenge, standardisierte Reihenfolge für den Zugriff auf und die Sperrung von Zeilen/Tabellen in der gesamten Anwendung fest.
Indizierung Stellen Sie sicher, dass alle für Lookups oder Updates verwendeten Spalten ordnungsgemäß indiziert sind, um zeilenweise Sperrung effizient zu nutzen.
Diagnose Überprüfen Sie regelmäßig die Ausgabe von SHOW ENGINE INNODB STATUS und die MySQL-Fehlerprotokolle auf wiederkehrende Deadlock-Muster.
Anwendungsbehandlung Implementieren Sie eine robuste Wiederholungslogik in der Anwendungsschicht, um den SQL-Fehler 1213 ordnungsgemäß zu behandeln.

Ein praktischer Debugging-Ablauf

Wenn ein Produktions-Deadlock-Alarm ausgelöst wird, ist ein nützlicher Ablauf:

  1. Erfassen Sie den Deadlock-Bericht, bevor er überschrieben wird.
  2. Identifizieren Sie die beiden Anweisungen und die beteiligten Indizes.
  3. Rekonstruieren Sie die Transaktion um jede Anweisung herum, nicht nur die einzelne Abfrage.
  4. Überprüfen Sie, ob die Codepfade Zeilen oder Tabellen in unterschiedlicher Reihenfolge sperren.
  5. Führen Sie EXPLAIN aus und fügen Sie Indizes hinzu oder passen Sie sie an, wenn der Scan breiter als erwartet ist.
  6. Stellen Sie sicher, dass eine Wiederholungslogik für sichere, idempotente Transaktionen vorhanden ist.

Deadlocks sind Teil von gleichzeitigen Transaktionssystemen, aber wiederholte Deadlocks sind normalerweise behebbar. Kürzere Transaktionen, konsistente Sperrreihenfolge, bessere Indizes und sorgfältige Wiederholungslogik bewirken mehr Gutes als das zufällige Optimieren von Servervariablen.