PostgreSQL-Fehler: deadlock detected
ERROR: deadlock detected
DETAIL: Process 18461 waits for ShareLock on transaction 1064; blocked by process 18463.
Process 18463 waits for ShareLock on transaction 1063; blocked by process 18461.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,3) in relation "accounts"
Zwei (oder mehr) Transaktionen warten jeweils auf einen Lock, den die andere hält. Keine kann jemals fortfahren, daher erkennt PostgreSQL den Zyklus, wählt eine Transaktion als Opfer aus und bricht sie mit diesem Fehler ab. Die andere Transaktion läuft normal weiter.
Was dieser Fehler bedeutet
Sperren auf Zeilenebene werden in PostgreSQL gehalten, bis die Transaktion committet oder zurückrollt. Wenn Transaktion A Zeile 1 sperrt und dann Zeile 2 will, während Transaktion B Zeile 2 hält und Zeile 1 will, bilden sie einen Zyklus: einen Deadlock. Wenn ein Backend länger als deadlock_timeout (Standard: 1 Sekunde) auf einen Lock gewartet hat, führt PostgreSQL eine Deadlock-Prüfung durch; findet es einen Zyklus, bricht es einen der Beteiligten mit SQLSTATE 40P01, dem Geschwister von 40001, ab.
Wichtige Einordnung: Die Datenbank hat die Situation bereits aufgelöst, wenn Sie den Fehler sehen. Nichts ist beschädigt, und die überlebende Transaktion hat ihre Lock-Wartezeit abgeschlossen. Was bleibt, ist ein Anwendungsproblem — die abgebrochene Transaktion wurde zurückgerollt und ihre Arbeit ist verloren, deshalb muss Ihre Anwendung darauf vorbereitet sein, sie zu wiederholen.
Häufige Ursachen
- Dieselben Zeilen in unterschiedlicher Reihenfolge aktualisieren. Der Klassiker: Job A aktualisiert Konto 1 dann Konto 2, Job B aktualisiert Konto 2 dann Konto 1. Zwei beliebige Schreiber mehrerer Zeilen ohne vereinbarte Reihenfolge können einen Deadlock erzeugen.
- Tabellen in unterschiedlicher Reihenfolge anfassen über verschiedene Codepfade hinweg — z. B. schreibt ein Pfad zuerst
ordersdanninventory, ein anderer zuerstinventorydannorders. - Fremdschlüssel. Das Einfügen oder Aktualisieren einer Kindzeile nimmt einen Share-Lock auf die referenzierte Elternzeile. Gemischt mit gleichzeitigen Aktualisierungen des Elternteils entstehen Deadlocks, die geheimnisvoll wirken, weil keine Query beide Tabellen erwähnt.
- Lock-Upgrades: Eine Zeile mit
SELECT ... FOR SHARElesen und sie später aktualisieren lässt zwei Transaktionen den Share-Lock erwerben und sich dann beim Upgrade gegenseitig blockieren. - Lange Transaktionen verursachen für sich genommen keine Deadlocks, aber sie halten Locks länger und vergrößern das Zeitfenster für jedes der obigen Muster.
So diagnostizieren Sie ihn
Der Fehler selbst enthält das meiste, was Sie brauchen:
DETAILlistet die Prozess-IDs auf und auf welche Transaktion jede gewartet hat.CONTEXTnennt oft das Tupel und die Relation, die geschrieben wurde.- Das Server-Log zu diesem Zeitpunkt zeichnet die beteiligten Queries auf — darauf verweist der HINT.
Setzen Sie log_lock_waits = on, um auch jede Lock-Wartezeit zu protokollieren, die länger als deadlock_timeout dauert; das zeigt Ihnen die Beinahe-Kollisionen, nicht nur die tatsächlichen. Für die Live-Untersuchung, wer wen blockiert:
SELECT waiting.pid AS waiting_pid, waiting.query AS waiting_query,
blocking.pid AS blocking_pid, blocking.query AS blocking_query
FROM pg_stat_activity waiting
JOIN pg_stat_activity blocking
ON blocking.pid = ANY (pg_blocking_pids(waiting.pid));
So beheben Sie ihn
- Einigen Sie sich auf eine globale Lock-Reihenfolge. Wenn eine Transaktion mehrere Zeilen schreibt, erwerben Sie die Locks zuerst in einer deterministischen Reihenfolge:
SELECT id FROM accounts WHERE id IN (7, 3, 42) ORDER BY id FOR UPDATE; -- dann die UPDATEs, in beliebiger Reihenfolge - Fassen Sie Tabellen überall in derselben Reihenfolge an (dokumentieren Sie die Reihenfolge; welche es ist, spielt keine Rolle, nur dass sie konsistent ist).
- Halten Sie Transaktionen kurz. Warten Sie nie auf Benutzereingaben, HTTP-Aufrufe oder Queues, während Sie Zeilen-Locks halten.
- Wiederholen Sie bei SQLSTATE
40P01— die gesamte Transaktion, nicht nur die fehlgeschlagene Anweisung, mit einem kleinen Backoff und einer begrenzten Anzahl von Versuchen. - Fassen Sie nach Möglichkeit Read-Modify-Write in einer einzigen Anweisung zusammen (
UPDATE ... SET x = x - 1 WHERE ...): eine einzige Anweisung erwirbt ihre Locks in einem Durchgang und ist viel schwerer in einen Deadlock zu bringen.
