aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Game-Server

MySQL Deadlock: Was es ist und wie man es löst

Ein MySQL Deadlock entsteht, wenn zwei oder mehr Transaktionen versuchen, dieselben Zeilen in entgegengesetzter Reihenfolge zu sperren, und endlos aufeinander warten. InnoDB erkennt diese Sackgasse, wählt eine Seite als "Opfer" und rollt sie zurück; deine Anwendung sieht dann den Fehler Deadlock found when trying to get lock; try restarting transaction. Auf einem Game-Server, einem E-Commerce-Backend oder jedem System mit hohem Schreibverkehr wirkt dieser Fehler unvermeidlich, doch die überwiegende Mehrheit der Deadlocks lässt sich durch besseres Transaktionsdesign verhindern.

Was ist ein Deadlock genau?

Das klassische Szenario läuft so ab: Transaktion A sperrt zuerst Zeile 1 und fordert dann Zeile 2 an. Im selben Moment hat Transaktion B bereits Zeile 2 gesperrt und fordert nun Zeile 1 an. Jede wartet auf eine Sperre, die die andere hält, also kommt niemand voran. Das ist ein circular wait (zirkuläres Warten).

Ein wichtiger Unterschied: Ein Deadlock ist nicht dasselbe wie ein Lock Wait Timeout. Bei einem Lock Wait Timeout wartet eine einzelne Transaktion auf die Freigabe einer Sperre und erhält einen Fehler, sobald innodb_lock_wait_timeout (Standard 50 Sekunden) abläuft. Bei einem Deadlock ist das Warten zirkulär, deshalb wartet InnoDB gar nicht — es greift sofort ein und rollt eine Seite zurück.

Warum entstehen InnoDB-Sperren?

InnoDB verwendet Sperren auf Zeilenebene, aber Sperren betreffen nicht immer "genau eine Zeile". Einige zentrale Verhaltensweisen erzeugen Deadlocks:

  • UPDATE/DELETE ohne Index: Kann eine WHERE-Klausel keinen passenden Index nutzen, sperrt InnoDB jede durchsuchte Zeile. Es sieht aus, als würdest du eine Zeile ändern, doch womöglich hast du Tausende gesperrt.
  • Gap- und Next-Key-Sperren: Auf der Isolationsstufe REPEATABLE READ (MySQLs Standard) sperrt InnoDB nicht nur Zeilen, sondern auch die "Lücken" dazwischen. Das verhindert Phantom Reads, erzeugt aber unerwartete Konflikte.
  • Fremdschlüssel- und Unique-Index-Prüfungen: Beim Einfügen oder Aktualisieren einer Zeile sperrt InnoDB auch die zugehörigen Eltern-/Kind-Schlüsselzeilen.
  • Inkonsistente Zugriffsreihenfolge: Transaktionen, die dieselben Tabellen in unterschiedlichen Reihenfolgen über verschiedene Codepfade aktualisieren, sind die mit Abstand häufigste Ursache für Deadlocks.

Das Deadlock-Log lesen

Bevor du rätst, lies, was InnoDB dir sagt. Die Details des jüngsten Deadlocks bekommst du mit:

SHOW ENGINE INNODB STATUS\G

Suche im Output den Abschnitt LATEST DETECTED DEADLOCK. Er zeigt die beiden Transaktionen, die von ihnen gehaltenen Sperren (HOLDS THE LOCK(S)), die Sperren, auf die sie warten (WAITING FOR THIS LOCK), und welche Abfrage lief. Zu sehen, welche Tabelle und welcher Index gesperrt wurde, ist die halbe Lösung.

Um jeden Deadlock dauerhaft zu protokollieren, füge dies zu deiner MySQL-Konfiguration hinzu:

[mysqld]
innodb_print_all_deadlocks = ON

Nun landet jeder Deadlock im MySQL-Error-Log, sodass du auch seltene Konflikte im Nachhinein untersuchen kannst.

Lösung 1: Eine konsistente Transaktionsreihenfolge festlegen

Die meisten Deadlocks entstehen durch inkonsistente Sperrreihenfolge. Die Lösung ist einfach, erfordert aber Disziplin: Lass jede Transaktion Ressourcen immer in derselben Reihenfolge berühren. Beim Übertragen eines Guthabens zwischen zwei Konten etwa sperrst du Zeilen stets von der kleineren id zur größeren:

START TRANSACTION;

-- Die kleinere id wird immer zuerst gesperrt
UPDATE accounts SET balance = balance - 100
  WHERE id = LEAST(@from, @to);
UPDATE accounts SET balance = balance + 100
  WHERE id = GREATEST(@from, @to);

COMMIT;

Selbst wenn zwei Transaktionen dieselben zwei Zeilen berühren, kann kein zirkuläres Warten entstehen, weil beide nun in derselben Reihenfolge ablaufen. Diese Regel verlangt, dass du überall in deinem Code dieselbe Zugriffsreihenfolge anwendest.

Lösung 2: Transaktionen klein und kurz halten

Je länger eine Transaktion offen bleibt, desto länger hält sie ihre Sperren und desto höher ist die Konfliktwahrscheinlichkeit. Praktische Regeln:

  • Stelle keine HTTP-Anfragen, schreibe keine Dateien und rufe keine externen APIs innerhalb einer Transaktion auf. Verlagere langsame Arbeit nach außen.
  • Bringe nur Schreibvorgänge in eine Transaktion, die wirklich atomar sein müssen.
  • Verwende SELECT ... FOR UPDATE nicht unnötig; sperre nur die Zeile, die du tatsächlich aktualisierst.
  • Teile Massen-Updates in kleine Batches auf, statt in eine einzige riesige Abfrage.

Lösung 3: Die richtigen Indizes hinzufügen

Eine WHERE-Klausel ohne Index zwingt InnoDB, weit mehr Zeilen als nötig zu sperren. Prüfe mit EXPLAIN, welchen Index deine Abfrage nutzt:

EXPLAIN UPDATE orders SET status = 'shipped'
  WHERE customer_id = 42 AND status = 'paid';

Zeigt die Spalte type den Wert ALL, findet ein vollständiger Tabellenscan statt. Ein passender zusammengesetzter Index auf customer_id und status reduziert die Zahl der gesperrten Zeilen auf ein Minimum und senkt das Deadlock-Risiko erheblich.

Lösung 4: Wiederholung auf Anwendungsseite

Deadlocks auf null zu drücken ist oft unrealistisch; seltene Konflikte können immer auftreten. Deine Anwendung sollte den Deadlock-Fehler daher abfangen und die Transaktion erneut versuchen. In Laravel geht das von Haus aus:

use Illuminate\Support\Facades\DB;

// Drittes Argument: bei Deadlock bis zu 3-mal wiederholen
DB::transaction(function () {
    DB::table('accounts')->where('id', 1)->decrement('balance', 100);
    DB::table('accounts')->where('id', 2)->increment('balance', 100);
}, 3);

Wichtiger Punkt: Eine Wiederholung ist nur für die Seite sinnvoll, die als Opfer gewählt wurde, und sie funktioniert, wenn die Transaktion kurz ist. Retry ist nicht dazu da, schlechtes Design zu kaschieren, sondern um die seltenen, unvermeidbaren Konflikte abzufedern.

Häufige Fragen

Verursacht ein Deadlock Datenverlust?

Nein. InnoDB rollt die Opfertransaktion vollständig zurück, sodass keine halbfertige Änderung zurückbleibt. Die eigentliche Gefahr ist, dass deine Anwendung den Fehler verschluckt und nie erneut versucht, sodass die Änderung des Nutzers stillschweigend verschwindet. Genau deshalb ist Retry-Logik unerlässlich.

Behebt das Erhöhen von innodb_lock_wait_timeout Deadlocks?

Nein. Diese Einstellung gilt für Lock Wait Timeouts, nicht für Deadlocks. InnoDB erkennt und löst Deadlocks bereits sofort; ein höherer Timeout betrifft nur Timeout-Szenarien und verhindert keine echten Deadlocks.

Beendet eine Tabellensperre die Deadlocks?

Technisch kann eine grobe Sperre Konflikte reduzieren, doch sie zerstört die Nebenläufigkeit und verlangsamt deinen Server drastisch. Der richtige Weg sind Zeilensperren in konsistenter Reihenfolge mit kurzen Transaktionen; eine Tabellensperre ist fast immer die falsche Lösung.

Wiederkehrende Deadlocks auf deinem Server? Wir können deine InnoDB-Logs gemeinsam prüfen und deine Transaktionsreihenfolge sowie Indizes korrigieren. Kontaktiere mich und machen wir deine Datenbank stabil.

Bu kategorideki tüm yazılar →

Devamı için