1. Was ist ein MySQL Deadlock?

Ein MySQL Deadlock entsteht, wenn zwei oder mehr Transaktionen jeweils eine Ressource halten und gleichzeitig auf eine Ressource warten, die die andere Transaktion hält. Das ergibt einen zyklischen Wartegraph, aus dem keine Transaktion selbstständig herauskommt. InnoDB erkennt diesen Zyklus typischerweise innerhalb von Millisekunden, wählt eine Opfer-Transaktion (die mit dem geringsten Rollback-Aufwand) und rollt diese zurück — die andere kann dann weitermachen.

Das klassische Szenario mit zwei Transaktionen: Transaktion A sperrt Zeile 1 und wartet auf Zeile 2. Transaktion B hat Zeile 2 gesperrt und wartet auf Zeile 1. Beide warten aufeinander — zyklischer Deadlock. InnoDB wählt Transaktion B als Opfer, rollt sie zurück, Transaktion A kann weitermachen. Der Aufrufer von Transaktion B erhält den Fehler ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction.

Deadlocks sind kein Zeichen eines kaputten Systems — sie sind ein normales Ergebnis von Concurrency. Aber häufige Deadlocks deuten auf schlechtes Transaktionsdesign, fehlende Indizes oder falschen Zugriffsreihenfolgen hin. Das Ziel ist nicht, alle Deadlocks auszuschließen (unmöglich), sondern ihre Häufigkeit unter eine akzeptable Schwelle zu senken und Anwendungen robust mit Retry-Logik auszustatten.

InnoDB verwendet Row-Level Locking — genauer: Es sperrt nicht nur Zeilen, sondern auch Gap Locks (Lücken zwischen Index-Einträgen) und Next-Key Locks (Kombination aus Record Lock und Gap Lock links davon). Das schützt vor Phantom Reads im REPEATABLE READ Isolation Level, kann aber bei vielen gleichzeitigen INSERTs in ähnliche Key-Bereiche zu Gap-Lock-Deadlocks führen — auch ohne dass die Transaktionen dieselben Zeilen anfassen.

2. SHOW ENGINE INNODB STATUS lesen

Der erste Anlaufpunkt nach einem Deadlock ist SHOW ENGINE INNODB STATUS\G. Das Kommando gibt einen langen Statusbericht aus, der im Abschnitt LATEST DETECTED DEADLOCK den zuletzt aufgezeichneten Deadlock zeigt — mit beiden beteiligten Transaktionen, ihren gehaltenen Locks und dem Lock, auf den sie gewartet haben.

-- Execute in MySQL CLI or Workbench
SHOW ENGINE INNODB STATUS\G

-- Relevant excerpt from the output:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2025-01-10 14:32:17 139742821234688
*** (1) TRANSACTION:
TRANSACTION 10523771, ACTIVE 0 sec starting index read
MySQL thread id 4821, OS thread handle 139742821234688, query id 987432 app_user UPDATE
UPDATE cataloginventory_stock_item SET qty = qty - 1 WHERE product_id = 42 AND stock_id = 1

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 289 page no 12 n bits 80 index PRIMARY
  of table `magento`.`sales_order` trx id 10523771 lock_mode X locks rec but not gap
Record lock, heap no 7 PHYSICAL RECORD: n_fields 42; ...

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 301 page no 8 n bits 72 index PRIMARY
  of table `magento`.`cataloginventory_stock_item` trx id 10523771 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 8; ...

*** (2) TRANSACTION:
TRANSACTION 10523770, ACTIVE 0 sec starting index read
MySQL thread id 4820, OS thread handle 139742853212160, query id 987431 app_user UPDATE
UPDATE sales_order SET status = 'processing' WHERE entity_id = 1001

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 301 page no 8 n bits 72 index PRIMARY
  of table `magento`.`cataloginventory_stock_item` trx id 10523770 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 8; ...

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 289 page no 12 n bits 80 index PRIMARY
  of table `magento`.`sales_order` trx id 10523770 lock_mode X locks rec but not gap
Record lock, heap no 7 PHYSICAL RECORD: n_fields 42; ...

*** WE ROLL BACK TRANSACTION (2)

So liest du den Output: Transaction (1) hält einen Lock auf sales_order (Zeile entity_id=1001) und wartet auf cataloginventory_stock_item (product_id=42). Transaction (2) hält den Lock auf cataloginventory_stock_item und wartet auf sales_order. Klassischer Zyklus — InnoDB rollt Transaction (2) zurück. Die SQL-Statements direkt unter TRANSACTION sind die aktuell laufenden Befehle, nicht unbedingt die Ursache des Locks — die früheren Statements der Transaktion erzeugen die Locks.

Merke dir den Deadlock-Typ: lock_mode X locks rec but not gap bedeutet Record Lock (nur die konkrete Zeile), lock_mode X ohne Zusatz bedeutet Next-Key Lock (Zeile + Gap links davon). Gap-Lock-Deadlocks treten häufig bei INSERT-Operationen auf und verschwinden oft durch Wechsel zum Isolation Level READ COMMITTED (falls fachlich vertretbar).

3. Deadlock-Logging konfigurieren

SHOW ENGINE INNODB STATUS speichert nur den letzten Deadlock. Bei häufigen Deadlocks überschreiben sich die Einträge gegenseitig. Mit innodb_print_all_deadlocks schreibt InnoDB jeden Deadlock ins MySQL Error Log — unverzichtbar für die Analyse intermittierender Probleme.

-- Enable logging of ALL deadlocks to the MySQL error log (MySQL 5.6+)
-- Set dynamically (takes effect immediately, no restart required):
SET GLOBAL innodb_print_all_deadlocks = ON;

-- Persist in my.cnf / my.ini for server restarts:
-- [mysqld]
-- innodb_print_all_deadlocks = ON

-- Verify current setting:
SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';
-- +-----------------------------+-------+
-- | Variable_name               | Value |
-- +-----------------------------+-------+
-- | innodb_print_all_deadlocks  | ON    |
-- +-----------------------------+-------+

-- Check the MySQL error log path:
SHOW VARIABLES LIKE 'log_error';
-- Typically: /var/log/mysql/error.log or /var/lib/mysql/<hostname>.err

-- Filter deadlock entries from the error log (run in shell):
-- grep -A 40 "LATEST DETECTED DEADLOCK" /var/log/mysql/error.log | tail -200

-- Track cumulative deadlock count since server start:
SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';
-- +------------------+-------+
-- | Variable_name    | Value |
-- +------------------+-------+
-- | Innodb_deadlocks | 47    |
-- +------------------+-------+

Der Zähler Innodb_deadlocks gibt dir einen schnellen Überblick: Steigt er in Monitoring-Systemen (Prometheus, Datadog, Zabbix) sprunghaft an, deutet das auf einen neuen Problembereich hin. Kombiniere ihn mit Innodb_row_lock_waits und Innodb_row_lock_time_avg für ein vollständiges Lock-Bild.

Für Magento-Shops lohnt es sich, einen Alert bei mehr als 10 Deadlocks pro Minute einzurichten — insbesondere während Promo-Events oder Flash-Sales, wenn viele Bestellungen gleichzeitig dasselbe Inventory-Objekt aktualisieren.

4. performance_schema.data_locks — Live-Lock-Inspektion

Während ein Deadlock läuft, kannst du aktive Locks in Echtzeit über das performance_schema inspizieren. Das ist besonders nützlich bei langen Transaktionen, die andere blockieren, aber noch keinen vollständigen Deadlock ausgelöst haben.

-- Show all currently held row locks (MySQL 8.0+):
SELECT
    dl.ENGINE_LOCK_ID,
    dl.ENGINE_TRANSACTION_ID,
    dl.THREAD_ID,
    dl.OBJECT_SCHEMA,
    dl.OBJECT_NAME,
    dl.LOCK_TYPE,    -- TABLE or RECORD
    dl.LOCK_MODE,    -- X, S, IX, IS, X,GAP, X,REC_NOT_GAP
    dl.LOCK_STATUS,  -- GRANTED or WAITING
    dl.LOCK_DATA     -- affected key value(s)
FROM performance_schema.data_locks dl
ORDER BY dl.ENGINE_TRANSACTION_ID, dl.LOCK_STATUS DESC;

-- Show lock wait chains: who is blocking whom?
SELECT
    r.ENGINE_TRANSACTION_ID  AS waiting_trx,
    r.THREAD_ID              AS waiting_thread,
    r.LOCK_MODE              AS waiting_mode,
    r.OBJECT_NAME            AS waiting_table,
    b.ENGINE_TRANSACTION_ID  AS blocking_trx,
    b.THREAD_ID              AS blocking_thread,
    b.LOCK_MODE              AS blocking_mode
FROM performance_schema.data_lock_waits dlw
JOIN performance_schema.data_locks r
     ON r.ENGINE_LOCK_ID = dlw.REQUESTING_ENGINE_LOCK_ID
JOIN performance_schema.data_locks b
     ON b.ENGINE_LOCK_ID = dlw.BLOCKING_ENGINE_LOCK_ID
ORDER BY blocking_trx;

-- Cross-reference with processlist to find the actual SQL:
SELECT
    p.ID,
    p.USER,
    p.HOST,
    p.DB,
    p.TIME,
    p.STATE,
    LEFT(p.INFO, 200) AS current_query
FROM information_schema.PROCESSLIST p
JOIN performance_schema.data_lock_waits dlw
     ON p.ID = dlw.WAITING_THREAD_ID
ORDER BY p.TIME DESC;

Mit dieser Kombination kannst du in einem Produktionsproblem innerhalb von Sekunden feststellen, welche Session wen blockiert, welche Tabellen und Zeilen betroffen sind und seit wie vielen Sekunden der Lock gehalten wird. Der LOCK_DATA-Wert zeigt den Primary-Key-Wert der gesperrten Zeile — damit findest du den konkreten Datensatz.

5. Magento Inventar-Deadlocks — Ursache und Lösung

Der häufigste MySQL Deadlock in Magento 2 entsteht beim gleichzeitigen Checkout mehrerer Kunden für dasselbe Produkt. Der typische Ablauf: Beim Bestelleingang sperrt Magento zuerst cataloginventory_stock_item (Inventory-Update), dann sales_order (Order-Anlage). Eine zweite parallele Bestellung kann in umgekehrter Reihenfolge zugreifen — Deadlock.

-- ─── Deadlock simulation: two sessions, different table access order ───

-- Session A (order processing starts with sales_order):
START TRANSACTION;
UPDATE sales_order
SET    status = 'processing', updated_at = NOW()
WHERE  entity_id = 1001;                          -- locks sales_order row

-- Session B (order processing starts with stock_item):
START TRANSACTION;
UPDATE cataloginventory_stock_item
SET    qty = qty - 1
WHERE  product_id = 42 AND stock_id = 1;          -- locks stock_item row

-- Now Session A tries to lock stock_item → waits for Session B
UPDATE cataloginventory_stock_item
SET    qty = qty - 1
WHERE  product_id = 42 AND stock_id = 1;          -- WAITING

-- Now Session B tries to lock sales_order → DEADLOCK!
UPDATE sales_order
SET    status = 'processing', updated_at = NOW()
WHERE  entity_id = 1001;                          -- DEADLOCK: InnoDB rolls back Session B

-- ─── Solution: enforce consistent lock order in Magento ───
-- Always acquire locks in the same order: stock_item FIRST, then sales_order

START TRANSACTION;

-- Step 1: Lock inventory row first (consistent order)
SELECT qty, is_in_stock
FROM   cataloginventory_stock_item
WHERE  product_id = 42 AND stock_id = 1
FOR UPDATE;                                       -- X-lock acquired

-- Step 2: Validate stock before proceeding
-- (application layer: check qty > 0, abort if out of stock)

-- Step 3: Decrement stock
UPDATE cataloginventory_stock_item
SET    qty = qty - 1,
       is_in_stock = IF(qty - 1 > 0, 1, 0)
WHERE  product_id = 42 AND stock_id = 1;

-- Step 4: Create/update order (lock acquired second, consistent order)
UPDATE sales_order
SET    status = 'processing', updated_at = NOW()
WHERE  entity_id = 1001;

COMMIT;

In Magento 2 mit MSI (Multi-Source Inventory) wurde die Inventory-Verarbeitung in separate Service-Layer ausgelagert. Die Reservierungen in inventory_reservation verwenden INSERT statt UPDATE, was Gap-Lock-Probleme mit sich bringen kann. Stelle sicher, dass product_id und stock_id in cataloginventory_stock_item einen kombinierten Index haben — fehlende Indizes zwingen InnoDB zu Table Scans mit Table-Level Locks statt Row Locks.

Ein weiterer Hotspot in Magento ist catalog_product_entity bei gleichzeitigen Re-Indexierungen und Frontend-Zugriffen. Trenne Indexier-Jobs zeitlich (Cronjob außerhalb der Spitzenlastzeiten) und nutze den Async-Indexer, um Schreibkonflikte zu reduzieren.

6. Konsistente Lock-Reihenfolge — Das wichtigste Mittel

Das mächtigste Werkzeug gegen MySQL Deadlocks ist die konsistente Lock-Reihenfolge. Wenn alle Transaktionen, die auf dieselben Ressourcen zugreifen, diese immer in derselben Reihenfolge sperren, kann kein zyklischer Wartegraph entstehen. Das klingt trivial, wird aber in komplexen Applikationen mit vielen Code-Pfaden schnell vergessen.

Definiere eine globale Lock-Hierarchie für deine Applikation: zum Beispiel immer zuerst nach Tabellen-Reihenfolge (alphabetisch oder nach Abhängigkeitsbaum), dann innerhalb einer Tabelle nach Primary Key aufsteigend. Dokumentiere diese Hierarchie und erzwinge sie durch Code-Reviews.

-- ─── Anti-pattern: inconsistent lock order leads to deadlock ───

-- Transaction 1 locks customers A, B in order: 101, 102
UPDATE customer_entity SET updated_at = NOW() WHERE entity_id = 101;
UPDATE customer_entity SET updated_at = NOW() WHERE entity_id = 102;

-- Transaction 2 locks customers in reverse order: 102, 101
UPDATE customer_entity SET updated_at = NOW() WHERE entity_id = 102; -- held
UPDATE customer_entity SET updated_at = NOW() WHERE entity_id = 101; -- DEADLOCK

-- ─── Solution: always lock rows in ascending Primary Key order ───

-- When processing multiple rows, sort IDs first in the application layer,
-- then lock in ascending order:

-- Transaction 1: lock IDs [101, 102] → always ascending → 101, then 102
START TRANSACTION;
SELECT entity_id FROM customer_entity
WHERE  entity_id IN (101, 102)
ORDER BY entity_id ASC                            -- enforce ascending order
FOR UPDATE;                                       -- acquire all locks upfront

UPDATE customer_entity SET updated_at = NOW() WHERE entity_id = 101;
UPDATE customer_entity SET updated_at = NOW() WHERE entity_id = 102;
COMMIT;

-- Transaction 2: same IDs, same ascending order → no cycle possible
START TRANSACTION;
SELECT entity_id FROM customer_entity
WHERE  entity_id IN (101, 102)
ORDER BY entity_id ASC                            -- same order as Transaction 1
FOR UPDATE;

UPDATE customer_entity SET updated_at = NOW() WHERE entity_id = 101;
UPDATE customer_entity SET updated_at = NOW() WHERE entity_id = 102;
COMMIT;

-- ─── Cross-table lock ordering: always same table sequence ───
-- Rule: catalog_product_entity → cataloginventory_stock_item → sales_order
-- Never deviate from this sequence, regardless of code path

Der SELECT ... FOR UPDATE am Transaktionsanfang hat einen weiteren Vorteil: Er akquiriert alle benötigten Locks auf einmal, bevor die eigentliche Verarbeitung beginnt. Damit ist sichergestellt, dass die Transaktion entweder alle Locks bekommt oder sofort wartet — ohne dass zwischenzeitlich andere Locks hinzukommen, die einen Zyklus erzeugen könnten.

7. Kürzere Transaktionen — Lock-Haltedauer minimieren

Je länger eine Transaktion läuft, desto länger hält sie ihre Locks und desto höher ist die Wahrscheinlichkeit, dass eine andere Transaktion dieselben Zeilen benötigt und einen Deadlock auslöst. Das Rezept: Transaktionen so kurz wie möglich halten.

Konkrete Maßnahmen: Externe Aufrufe (HTTP, externe APIs, Dateisystem) vor oder nach der Transaktion durchführen, nie innerhalb. Berechnungen und Validierungen vor dem Transaktionsbeginn abschließen. Keine Transactions über mehrere HTTP-Requests hinweg halten (Session-Transactions). Batch-Verarbeitungen in kleinere Chunks aufteilen.

-- ─── Anti-pattern: long transaction with external operations ───

-- BAD: Transaction stays open while slow operations run
START TRANSACTION;

UPDATE sales_order SET status = 'processing' WHERE entity_id = 1001;
-- ... application code calls external payment API (takes 2-5 seconds!) ...
-- ... application code sends confirmation email ...
-- ... application code updates analytics service ...

UPDATE cataloginventory_stock_item SET qty = qty - 1 WHERE product_id = 42;

COMMIT; -- locks held for 2-5+ seconds → high deadlock probability

-- ─── Solution: do all slow work BEFORE opening the transaction ───

-- Step 1: Validate and prepare OUTSIDE transaction
-- (application layer: validate payment, prepare email content, etc.)

-- Step 2: Short, focused transaction
START TRANSACTION;

UPDATE cataloginventory_stock_item               -- lock stock first (consistent order)
SET    qty = qty - 1,
       is_in_stock = IF(qty - 1 > 0, 1, 0)
WHERE  product_id = 42 AND stock_id = 1;

UPDATE sales_order
SET    status = 'processing', updated_at = NOW()
WHERE  entity_id = 1001;

-- Insert an event for async processing instead of doing it inline:
INSERT INTO sales_order_status_history
    (parent_id, status, comment, is_customer_notified, created_at)
VALUES
    (1001, 'processing', 'Order confirmed', 0, NOW());

COMMIT; -- locks held for < 10 ms → minimal deadlock window

-- Step 3: Send email and call APIs AFTER commit
-- (application layer: asynchronous email queue, event dispatch)

In Magento 2 ist das Message Queue System (RabbitMQ oder MySQL-basiert) genau für diesen Zweck gedacht: Schwere Operationen wie E-Mail-Versand, ERP-Synchronisation oder Lagerverwaltung werden als Nachrichten in die Queue geschrieben und asynchron verarbeitet. Die Datenbank-Transaktion bleibt minimal.

8. Optimistic Locking — Version-Column statt FOR UPDATE

Pessimistic Locking (SELECT ... FOR UPDATE) sperrt eine Zeile sofort beim Lesen und hält den Lock bis zum COMMIT. Das ist sicher, aber bei hoher Concurrency ein Flaschenhals und Deadlock-Risikoerhöher. Optimistic Locking geht davon aus, dass Konflikte selten sind: Man liest ohne Lock, verarbeitet die Daten, und überprüft beim Schreiben, ob sich die Daten zwischenzeitlich verändert haben — meist mit einer Versionsspalte.

-- ─── Optimistic Locking with a version column ───

-- Add a version column to the table (integer or timestamp):
ALTER TABLE cataloginventory_stock_item
ADD COLUMN version INT UNSIGNED NOT NULL DEFAULT 0
AFTER is_in_stock;

-- Read without any lock (no FOR UPDATE):
SELECT product_id, stock_id, qty, is_in_stock, version
FROM   cataloginventory_stock_item
WHERE  product_id = 42 AND stock_id = 1;
-- Returns: qty = 10, version = 7

-- Application layer: validate business logic (qty > 0, etc.)

-- Update: include version in WHERE clause as conflict guard
UPDATE cataloginventory_stock_item
SET    qty          = qty - 1,
       is_in_stock  = IF(qty - 1 > 0, 1, 0),
       version      = version + 1              -- increment version
WHERE  product_id   = 42
  AND  stock_id     = 1
  AND  version      = 7;                      -- only update if version unchanged

-- Check affected rows (application layer):
-- affected_rows = 1 → success, our update went through
-- affected_rows = 0 → conflict! Another transaction updated first → retry

-- ─── Retry logic in application pseudocode ───
-- max_retries = 3
-- for attempt in range(max_retries):
--     row = SELECT ... (no lock)
--     new_qty = row.qty - 1
--     affected = UPDATE ... WHERE version = row.version
--     if affected == 1:
--         break   # success
--     else:
--         sleep(exponential_backoff(attempt))
-- else:
--     raise StockUpdateException("Optimistic lock failed after retries")

Optimistic Locking eliminiert Deadlocks vollständig für die betroffenen Operationen — es gibt keine exklusiven Read-Locks, also keinen zyklischen Wartegraph möglich. Der Preis: Bei tatsächlichen Konflikten muss die Applikation die Operation wiederholen. Das ist akzeptabel, wenn Konflikte selten sind.

Für hochfrequente Updates auf denselben Zeilen (z.B. Inventory während Flash-Sales) kombiniere Optimistic Locking mit einem Inventory-Reservation-Pattern: Statt direkt qty zu dekrementieren, schreibt jede Bestellung eine Reservierung in eine separate Tabelle. Ein async Worker konsolidiert die Reservierungen periodisch. Das entkoppelt die Schreiblast und macht Deadlocks nahezu unmöglich.

9. NOWAIT und SKIP LOCKED — MySQL 8 Lock-Kontrolle

MySQL 8.0 führte NOWAIT und SKIP LOCKED als Erweiterungen für SELECT ... FOR UPDATE ein. Diese Klauseln geben Entwicklern feinere Kontrolle darüber, wie sich eine Transaktion verhält, wenn ein Lock nicht sofort verfügbar ist — und sind nützliche Werkzeuge zur Deadlock-Prävention.

-- ─── NOWAIT: fail immediately if lock is not available ───
-- Instead of waiting (and potentially deadlocking), return error 3572 immediately

START TRANSACTION;

SELECT entity_id, qty, is_in_stock
FROM   cataloginventory_stock_item
WHERE  product_id = 42 AND stock_id = 1
FOR UPDATE NOWAIT;                                -- immediate error if locked

-- If another transaction holds the lock, MySQL returns:
-- ERROR 3572 (HY000): Statement aborted because lock(s) could not be acquired
-- immediately and NOWAIT is set.

-- Application handles: retry after brief pause, or show "Try again" to user
ROLLBACK;

-- ─── SKIP LOCKED: job queue processing without deadlocks ───
-- Ideal for worker pools processing a queue table

-- Queue table structure (e.g., order processing queue):
CREATE TABLE order_processing_queue (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id   INT UNSIGNED NOT NULL,
    status     ENUM('pending', 'processing', 'done', 'failed') DEFAULT 'pending',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_status (status)
);

-- Worker picks up next available batch without waiting for locked rows:
START TRANSACTION;

SELECT id, order_id
FROM   order_processing_queue
WHERE  status = 'pending'
ORDER BY id ASC
LIMIT  10
FOR UPDATE SKIP LOCKED;                           -- skip rows locked by other workers

-- Multiple workers can run in parallel, each picking different rows
-- No waiting → no deadlocks → high throughput job processing

UPDATE order_processing_queue
SET    status = 'processing'
WHERE  id IN (/* ids from SELECT above */);

COMMIT;

-- ─── Difference summary ───
-- FOR UPDATE           → wait until lock available (default, may deadlock)
-- FOR UPDATE NOWAIT    → error immediately if locked (let app retry)
-- FOR UPDATE SKIP LOCKED → silently skip locked rows (perfect for queues)

SKIP LOCKED ist ideal für Worker-Pool-Szenarien: Mehrere parallele Worker-Prozesse greifen auf dieselbe Queue-Tabelle zu, ohne sich gegenseitig zu blockieren. Jeder Worker überspringt gesperrte Zeilen und holt sich die nächsten verfügbaren — kein Warten, kein Deadlock, hoher Durchsatz.

In Magento 2 implementiert das Magento\MysqlMq-Modul genau dieses Muster für die MySQL-basierte Message Queue. Wenn du eigene Queue-Verarbeitung implementierst, nutze SKIP LOCKED als Drop-in-Lösung für Worker-Deadlocks.

10. Deadlock-Monitoring in der Produktion

Reaktive Deadlock-Analyse (erst nachschauen, wenn Fehler gemeldet wurden) reicht nicht aus. Proaktives Monitoring gibt dir frühzeitig Signale, bevor Deadlocks die User Experience beeinträchtigen. Die wichtigsten Metriken und Queries:

-- ─── Deadlock rate monitoring (poll every 60 seconds) ───

-- Current cumulative deadlock count:
SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';

-- Lock wait statistics:
SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';
-- Innodb_row_lock_current_waits  → currently waiting transactions
-- Innodb_row_lock_time           → total time waiting for locks (ms)
-- Innodb_row_lock_time_avg       → avg lock wait time (ms) since start
-- Innodb_row_lock_time_max       → max single lock wait time (ms)
-- Innodb_row_lock_waits          → total number of lock waits

-- ─── Real-time lock wait analysis ───

-- Transactions currently waiting for locks (MySQL 8):
SELECT
    trx.trx_id,
    trx.trx_started,
    trx.trx_wait_started,
    trx.trx_mysql_thread_id,
    trx.trx_query,
    TIMESTAMPDIFF(SECOND, trx.trx_wait_started, NOW()) AS wait_seconds
FROM information_schema.INNODB_TRX trx
WHERE trx.trx_state = 'LOCK WAIT'
ORDER BY wait_seconds DESC;

-- Lock waits with blocking/waiting info (MySQL 8 performance_schema):
SELECT
    dlw.REQUESTING_ENGINE_TRANSACTION_ID  AS waiting_trx,
    dlw.BLOCKING_ENGINE_TRANSACTION_ID    AS blocking_trx,
    r.OBJECT_NAME                         AS locked_table,
    r.LOCK_MODE                           AS waiting_mode,
    b.LOCK_MODE                           AS blocking_mode
FROM performance_schema.data_lock_waits   dlw
JOIN performance_schema.data_locks        r
     ON r.ENGINE_LOCK_ID = dlw.REQUESTING_ENGINE_LOCK_ID
JOIN performance_schema.data_locks        b
     ON b.ENGINE_LOCK_ID = dlw.BLOCKING_ENGINE_LOCK_ID;

-- Check replication lag if deadlocks affect replica:
SHOW REPLICA STATUS\G
-- Check Seconds_Behind_Source: deadlock rollbacks on primary
-- can cause replication lag if many rows are being re-processed

-- ─── Alert thresholds (example values, adjust to your traffic) ───
-- Innodb_deadlocks rate > 10/minute  → investigate immediately
-- Innodb_row_lock_time_avg > 500 ms  → index or query optimization needed
-- Innodb_row_lock_current_waits > 50 → potential storm, check lock chains

Für Magento-Shops empfehle ich, Deadlock-Ereignisse direkt in Magento-Logs zu erfassen: Fange Magento\Framework\DB\Adapter\DeadlockException und schreibe sie mit einem eigenen Logger in eine dedizierte Log-Datei inkl. Stack Trace und Query. Das gibt dir Context, den der MySQL Error Log allein nicht bietet — nämlich welcher Magento-Prozess (Checkout, Reindex, Import) den Deadlock ausgelöst hat.

Denke daran: Deadlocks sind in einem konkurrierenden System unvermeidbar. Dein Ziel ist nicht null Deadlocks, sondern eine Applikation, die Deadlocks erkennt, graceful mit Retry-Logik reagiert, und deren Deadlock-Rate unter einem akzeptablen Schwellenwert bleibt. Kombiniere alle hier beschriebenen Techniken — konsistente Lock-Reihenfolge, kurze Transaktionen, Optimistic Locking und SKIP LOCKED — für maximale Wirkung.

MySQL Deadlocks kosten deinen Shop bares Geld

Häufige Deadlocks im Checkout bedeuten fehlgeschlagene Bestellungen und frustrierte Kunden. Mit einem gezielten Lock-Audit lassen sich die meisten Deadlock-Ursachen in wenigen Stunden beseitigen.

Lock-Audit anfragen
Zusammenfassung: MySQL Deadlock verstehen und eliminieren
  • Ein MySQL Deadlock entsteht durch zyklischen Wartegraph — InnoDB erkennt und bricht ihn automatisch auf
  • SHOW ENGINE INNODB STATUS\G zeigt den letzten Deadlock mit HOLDS/WAITING-Sektionen beider Transaktionen
  • innodb_print_all_deadlocks = ON schreibt jeden Deadlock ins Error Log — unverzichtbar für Analyse
  • performance_schema.data_locks und data_lock_waits ermöglichen Live-Lock-Inspektion
  • Magento-Deadlocks entstehen oft durch inkonsistente Reihenfolge bei cataloginventory_stock_item und sales_order
  • Konsistente Lock-Reihenfolge (immer dieselbe Tabellen- und Zeilen-Sequenz) verhindert zyklische Wartegraphen
  • Kurze Transaktionen minimieren die Lock-Haltedauer und das Überschneidungsfenster
  • Optimistic Locking mit Version-Column eliminiert Deadlocks für Szenarien mit seltenen Konflikten
  • FOR UPDATE NOWAIT schlägt sofort fehl statt zu warten; SKIP LOCKED überspringt gesperrte Zeilen
  • Monitoring via Innodb_deadlocks und Innodb_row_lock_* gibt frühzeitige Warnsignale
Häufige Fragen zu MySQL Deadlocks
▶ Was genau ist ein MySQL Deadlock und wann tritt er auf?

Ein MySQL Deadlock tritt auf, wenn zwei oder mehr Transaktionen sich gegenseitig blockieren: Transaktion A hält Lock X und wartet auf Lock Y, Transaktion B hält Lock Y und wartet auf Lock X. Dieser zyklische Wartegraph kann sich nicht selbst auflösen — InnoDB erkennt ihn und rollt eine der Transaktionen zurück (die mit dem geringsten Rollback-Aufwand). Der Aufrufer erhält Fehler 1213 und muss die Transaktion wiederholen.

▶ Wie lese ich SHOW ENGINE INNODB STATUS um einen Deadlock zu verstehen?

Suche im Output nach dem Abschnitt "LATEST DETECTED DEADLOCK". Dort findest du zwei Transaktionen (1) und (2). Unter "HOLDS THE LOCK(S)" siehst du, welche Zeilen jede Transaktion gesperrt hatte. Unter "WAITING FOR THIS LOCK TO BE GRANTED" siehst du, auf welchen Lock sie wartete. Am Ende steht "WE ROLL BACK TRANSACTION (N)" — das Deadlock-Opfer. Der SQL-Statement-Text zeigt den aktuell laufenden Befehl, nicht unbedingt den lock-erzeugenden.

▶ Was bringt innodb_print_all_deadlocks und wie aktiviere ich es?

innodb_print_all_deadlocks schreibt jeden aufgetretenen Deadlock ins MySQL Error Log — nicht nur den letzten wie bei SHOW ENGINE INNODB STATUS. Damit verlierst du keine Deadlock-Informationen bei häufigen Ereignissen. Aktivierung ohne Neustart: SET GLOBAL innodb_print_all_deadlocks = ON;. Für Persistenz in my.cnf unter [mysqld] eintragen: innodb_print_all_deadlocks = ON.

▶ Warum entstehen in Magento so häufig Deadlocks bei cataloginventory_stock_item?

Magento 2 aktualisiert beim Checkout mehrere Tabellen innerhalb einer Transaktion: Lager (cataloginventory_stock_item) und Order (sales_order). Wenn zwei parallele Checkout-Prozesse diese Tabellen in unterschiedlicher Reihenfolge sperren — einer zuerst stock_item, der andere zuerst sales_order — entsteht ein klassischer zyklischer Deadlock. Lösung: Immer dieselbe Tabellen-Reihenfolge erzwingen und fehlende Indizes auf product_id/stock_id prüfen, die sonst Table Scans mit breiten Locks verursachen.

▶ Was ist der Unterschied zwischen Pessimistic und Optimistic Locking?

Pessimistic Locking (SELECT ... FOR UPDATE) sperrt eine Zeile sofort beim Lesen exklusiv und hält den Lock bis zum COMMIT. Es geht davon aus, dass Konflikte häufig sind und blockiert concurrent Zugriffe. Optimistic Locking liest ohne Lock, verarbeitet die Daten und prüft beim Schreiben per Versionsspalte ob sich die Zeile inzwischen geändert hat. Bei Konflikt (affected_rows = 0) wiederholt die Applikation die Operation. Optimistic Locking ist besser für hohe Concurrency mit seltenen Konflikten und eliminiert Deadlocks für diese Operationen.

▶ Wann sollte ich FOR UPDATE NOWAIT statt normalem FOR UPDATE verwenden?

NOWAIT ist sinnvoll, wenn Warten keine Option ist und die Applikation lieber sofort mit einem Fehler reagiert als zu warten. Typische Anwendungsfälle: Web-Request-Handler mit strengem Timeout-Budget, UI-Aktionen die sofortiges Feedback erfordern, oder Szenarien wo ein anderer Code-Pfad die Aufgabe übernehmen kann. Die Applikation fängt dann Fehler 3572 und reagiert mit Retry-Logik oder einer benutzerfreundlichen Meldung.

▶ Wie funktioniert SKIP LOCKED für Job-Queue-Verarbeitung?

Bei SKIP LOCKED überspringt SELECT ... FOR UPDATE Zeilen, die von anderen Transaktionen gesperrt sind, statt zu warten. Das ermöglicht mehrere parallele Worker, die aus derselben Queue-Tabelle Jobs abholen: Jeder Worker bekommt einen anderen Batch ungesperrter Rows, ohne auf andere Workers zu warten. Kein Warten bedeutet keine Deadlocks. SKIP LOCKED ist seit MySQL 8.0 verfügbar und ideal für parallele Queue-Processing-Systeme.

▶ Was sind Gap Locks und warum verursachen sie Deadlocks bei INSERTs?

Gap Locks sperren nicht eine konkrete Zeile, sondern die Lücke zwischen zwei Index-Einträgen. InnoDB nutzt sie im REPEATABLE READ Isolation Level, um Phantom Reads zu verhindern. Bei gleichzeitigen INSERTs in ähnliche Key-Bereiche kann es passieren, dass Transaktion A den Gap vor Key 100 sperrt und Transaktion B den Gap vor Key 95 — und beide versuchen in denselben überlappenden Gap-Bereich zu inserieren. Das erzeugt einen Deadlock ohne dass die Transaktionen dieselben Rows anfassen. Lösung: READ COMMITTED Isolation Level (eliminiert Gap Locks) oder Tabellendesign anpassen (natürliche Keys statt INSERTs in dichte Key-Bereiche).

▶ Sollte die Applikation Deadlocks automatisch wiederholen?

Ja — Deadlock-Retry ist Best Practice. Da InnoDB den Fehler 1213 zurückgibt und die Transaktion vollständig rollt back, ist es sicher, die gesamte Transaktion zu wiederholen. Typische Retry-Strategie: 3 Versuche mit exponential Backoff (z.B. 50ms, 150ms, 450ms). Magento 2 implementiert teilweise eigene Retry-Mechanismen im DB-Adapter. Wichtig: Die gesamte Transaktion (nicht nur das letzte Statement) muss erneut ausgeführt werden, da der Rollback alle Änderungen rückgängig gemacht hat.

▶ Welche Monitoring-Metriken sind am wichtigsten für Deadlock-Prävention?

Die wichtigsten Metriken: Innodb_deadlocks (Gesamtzahl, Rate pro Minute), Innodb_row_lock_time_avg (durchschnittliche Lock-Wartezeit in ms, Alarm > 500ms), Innodb_row_lock_current_waits (aktuell wartende Transaktionen, Alarm > 50), Innodb_row_lock_waits (Gesamtanzahl Lock-Waits). Ergänze mit Live-Abfragen auf performance_schema.data_lock_waits bei akuten Problemen. Alert-Schwellwerte hängen vom Traffic ab — kalibriere sie an ruhigen Tagen.