iToverDose/Software· 7 JULI 2026 · 16:03

PostgreSQL Deadlocks: Warum die Datenbank erst nach Wartezeit eingreift

PostgreSQL erkennt Deadlocks nicht sofort, sondern erst nach einer Verzögerung. Wir erklären, wie der Mechanismus funktioniert, warum sinnlose Retries das Problem verschlimmern und wie Entwickler es richtig angehen.

DEV Community5 min0 Kommentare

PostgreSQL ist ein zuverlässiger Schiedsrichter für Datenbankoperationen – doch wenn Transaktionen sich gegenseitig blockieren, greift die Datenbank erst nach einer bestimmten Wartezeit ein. Das kann Entwickler im Live-Betrieb überraschen: Plötzlich erhalten Nutzer Fehler statt erfolgreicher Transaktionen. Doch warum wartet PostgreSQL, bevor es einen Deadlock auflöst? Und warum führen naive Retry-Mechanismen oft zu noch mehr Problemen?

Wie PostgreSQL Deadlocks erkennt – und warum es nicht sofort handelt

Ein Deadlock entsteht, wenn zwei oder mehr Transaktionen gegenseitig Ressourcen blockieren, die die jeweils andere benötigt. PostgreSQL erstellt intern einen Wartegraphen, der die Abhängigkeiten zwischen Transaktionen abbildet. Sobald ein Zyklus erkannt wird, bricht die Datenbank gezielt eine Transaktion ab, um die Blockade zu lösen. Doch dieser Prozess startet nicht sofort: Erst wenn eine Transaktion länger als die konfigurierte deadlock_timeout-Zeit (Standard: 1 Sekunde) auf eine Ressource wartet, wird der Deadlock-Check ausgelöst.

Diese Verzögerung ist kein Zufall, sondern eine bewusste Designentscheidung. Die PostgreSQL-Dokumentation erklärt, dass die meisten Sperren in normalen Workloads nur wenige Millisekunden halten – etwa beim Aktualisieren eines kurzen Datensatzes. Ein Deadlock-Check bei jeder Sperrenwartung wäre hier ineffizient und würde unnötig CPU-Ressourcen verbrauchen. Stattdessen setzt PostgreSQL auf eine lazy Detection: Nur wenn eine Transaktion tatsächlich länger blockiert als deadlock_timeout, wird der Graph-Algorithmus aktiviert. Die Konsequenz: Im Falle eines echten Deadlocks müssen alle betroffenen Transaktionen mindestens diese Zeitspanne warten, bevor die Blockade aufgelöst wird.

Der Algorithmus hinter der Deadlock-Erkennung

Sobald der Timer abläuft, konstruiert der betroffene Backend-Prozess einen Wartegraphen aus den aktuellen Sperren. Jeder Prozess, der auf eine Ressource wartet, wird als Knoten dargestellt, während die Kanten die Abhängigkeit zwischen den Prozessen abbilden. PostgreSQL prüft zunächst, ob sich die Sperren durch eine Umordnung der Warteschlangen auflösen lassen – etwa wenn zwei Transaktionen gleichzeitig auf dieselbe Ressource warten, aber unterschiedliche Prioritäten haben. Erst wenn keine Umordnung möglich ist, gilt ein Deadlock als bestätigt.

Die eigentliche Logik steckt in der Datei deadlock.c im PostgreSQL-Quellcode. Wird ein Zyklus erkannt, ist es der Prozess, der den Check ausführt, der abgebrochen wird. Dieser wirft dann den Fehler ERROR: deadlock detected (SQLSTATE 40P01) und rollt die Transaktion zurück. Die betroffenen Sperren werden freigegeben, und die anderen Prozesse im Zyklus können ihre Arbeit fortsetzen. Ein typisches Log-Eintrag sieht dann so aus:

LOG: process 12345 still waiting for ShareLock on transaction 9876 after 1000.123 ms
DETAIL: Process holding the lock: 12399. Wait queue: 12345.
CONTEXT: while updating tuple (5,12) in relation "accounts"
STATEMENT: UPDATE accounts SET balance = balance + 100 WHERE id = 99
ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 9876; blocked by process 12399.
        Process 12399 waits for ShareLock on transaction 9877; blocked by process 12345.
Process 12345: UPDATE accounts SET balance = balance + 100 WHERE id = 99
Process 12399: UPDATE accounts SET balance = balance + 50 WHERE id = 42
HINT: See server log for query details.

Im Metrik-View pg_stat_database wird jeder erkannte Deadlock im Zähler deadlocks erfasst. Eine Historie der Deadlocks ist jedoch nicht direkt abrufbar – wer diese Daten speichern möchte, muss die Logs auswerten.

Warum sinnlose Retries das Problem verschlimmern

Ein häufiger Fehler in Produktionsumgebungen ist die Implementierung eines Retry-Mechanismus, der das eigentliche Problem nicht löst. Entwickler stoßen auf den Fehler 40P01 und reagieren mit Code wie diesem:

def transfer(src_id, dst_id, amount):
    for attempt in range(10):
        try:
            with conn.transaction():
                conn.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", (amount, src_id))
                conn.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", (amount, dst_id))
            return
        except DeadlockDetected:
            time.sleep(0.01)  # Backoff zu kurz, Problem bleibt bestehen
    raise

Das Problem: Wenn zwei Transaktionspfade Ressourcen in unterschiedlicher Reihenfolge sperren – etwa transfer(from, to) mit der Reihenfolge from vor to – wird der Deadlock bei jedem Retry deterministisch neu erzeugt. Unter hoher Last summieren sich die Wartezeiten: Aus einer Transaktion, die nur wenige Millisekunden dauern sollte, wird durch fünf Retries eine Operation von mehreren Sekunden. Noch kritischer: Ohne exponentiellen Backoff und ohne Begrenzung der Retry-Versuche kann der Datenbankprozess in eine Endlosschleife geraten, in der ständig neue Deadlocks erkannt und verarbeitet werden. Die CPU wird dann mit Graph-Algorithmen statt mit eigentlichen Datenbankoperationen belastet.

Die Lösung liegt in der Normalisierung der Sperrreihenfolge. Jeder Codepfad, der mehrere Zeilen oder Tabellen betrifft, sollte die Ressourcen in einer festen Reihenfolge sperren – etwa nach aufsteigenden IDs oder alphabetisch nach Tabellennamen. So wird sichergestellt, dass Transaktionen unabhängig voneinander immer in derselben Reihenfolge auf Ressourcen zugreifen:

def transfer(src_id, dst_id, amount):
    lo, hi = sorted([src_id, dst_id])
    for attempt in range(3):
        try:
            with conn.transaction():
                # Sperren beider Zeilen in fester Reihenfolge
                conn.execute(
                    "SELECT id FROM accounts WHERE id IN (%s,%s) ORDER BY id FOR UPDATE",
                    (lo, hi),
                )
                conn.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", (amount, src_id))
                conn.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", (amount, dst_id))
            return
        except DeadlockDetected:
            time.sleep(0.05 * (2 ** attempt))  # Exponentieller Backoff
    raise

Retries sind weiterhin sinnvoll, um Race Conditions zu behandeln, die nicht durch die Reihenfolge der Sperren vermieden werden können. Allerdings müssen sie mit einer harten Begrenzung der Versuche, exponentiellem Backoff und Alerting kombiniert werden, um sicherzustellen, dass das Team über persistierende Probleme informiert wird.

Implizite Sperren durch Fremdschlüssel: Eine oft unterschätzte Falle

Nicht alle Deadlocks entstehen durch explizite Sperren. Ein häufiger Auslöser sind implizite Sperren bei Fremdschlüssel-Operationen. Wenn eine Transaktion einen neuen Datensatz mit einem Fremdschlüssel einfügt, der auf eine bestehende Zeile verweist, nimmt PostgreSQL automatisch eine FOR KEY SHARE-Sperre auf der referenzierten Zeile vor, um zu verhindern, dass diese Zeile während der Transaktion gelöscht oder geändert wird.

Zwei Transaktionen, die jeweils einen neuen Datensatz mit demselben Fremdschlüssel einfügen, kollidieren nicht direkt – beide halten nur eine FOR KEY SHARE-Sperre. Problematisch wird es jedoch, wenn eine Transaktion nach dem Einfügen eines Kind-Datensatzes die Elternzeile aktualisiert, während eine andere Transaktion gleichzeitig versucht, die Elternzeile zu löschen oder zu ändern. In diesem Fall können sich die Sperren zu einem Deadlock aufschaukeln, selbst wenn die Entwickler keine expliziten Sperren verwendet haben.

Entwickler sollten solche Szenarien antizipieren und entweder:

  • die Reihenfolge der Operationen anpassen, um Konflikte zu vermeiden,
  • explizite Sperren in der richtigen Reihenfolge setzen oder
  • komplexe Transaktionen in kleinere Einheiten aufteilen.

PostgreSQL bietet zwar robuste Mechanismen zur Deadlock-Erkennung, doch die eigentliche Herausforderung liegt in der Vermeidung von Konflikten durch sauberes Design. Indem Entwickler Sperrreihenfolgen standardisieren und implizite Sperren durch Fremdschlüssel bewusst berücksichtigen, können sie Deadlocks von vornherein minimieren – und so die Stabilität und Performance ihrer Datenbankanwendungen nachhaltig verbessern.

KI-Zusammenfassung

PostgreSQL'in deadlock tespit mekanizması, üretim ortamındaki yaygın hatalardan biri olan sonsuz retry döngüsünden kaçınma yolları ve kilit alma stratejileri hakkında detaylı bir rehber.

Kommentare

00
KOMMENTAR SCHREIBEN
ID #6WHV1R

0 / 1200 ZEICHEN

Menschen-Check

4 + 2 = ?

Erscheint nach redaktioneller Prüfung

Moderation · Spam-Schutz aktiv

Noch keine Kommentare. Sei der erste.