Praktický návod

Jak řešit deadlock v databázi

Deadlock není náhodná databázová chyba. Najdi opačné pořadí zámků, oprav ho a teprve potom přidej omezený retry.

25 minut · PostgreSQL

Nejdřív stručně

Dvě transakce čekají jedna na druhou

Deadlock vznikne, když dvě transakce drží rozdílné zámky a každá čeká na zámek té druhé. PostgreSQL cyklus rozpozná, jednu transakci ukončí a vrátí SQLSTATE 40P01.

Rollback oběti uvolní zámky, ale obchodní operace se nedokončí. Trvalá oprava je předvídatelné pořadí zamykání a krátká transakce; omezené opakování je jen ochrana proti nevyhnutelnému souběhu.

Připrav si

Co budeš potřebovat

K řešení potřebuješ obě strany konfliktu. Samotný stack trace jedné ukončené transakce obvykle nestačí.

  • PostgreSQL log s detailem deadlocku, SQLSTATE a časy dotčených dotazů.
  • Aplikační log s correlation ID, názvem use case a bezpečně zaznamenanými identifikátory záznamů.
  • Přístup k pg_stat_activity a pg_locks pro pozorování blokování bez změny databáze.
  • Integrační scénář, který umí spustit dvě konkurenční transakce proti stejným datům.

Kroky 1 až 3

Najdi cyklus a odstraň jeho příčinu

Nejdřív zrekonstruuj pořadí dotazů v obou transakcích. Potom sjednoť zamykání a přidej přesně vymezenou obnovu.

1. Potvrď deadlock a zrekonstruuj pořadí

  1. Rozliš SQLSTATE 40P01 deadlock_detected od běžného čekání na zámek, timeoutu a ztraceného spojení. Každá chyba potřebuje jinou reakci.
  2. Z databázového logu zjisti dotazy a zámky v obou procesech. V aplikačním logu je přiřaď ke konkrétním use case.
  3. Sepiš pořadí čtení a zápisů. Typický cyklus je: transakce A zamkne řádek 1 a chce 2, transakce B zamkne 2 a chce 1.
  4. Citlivé hodnoty parametrů do logu neposílej. Pro spojení událostí stačí correlation ID a interní identifikátory.
SELECT pid, wait_event_type, wait_event, state, query FROM pg_stat_activity WHERE datname = current_database();
Oficiální PostgreSQL dokumentace k pg_stat_activity

2. Zamykej data vždy ve stejném pořadí

  1. Urči jedno pořadí sdílené všemi use case, například podle typu zdroje a potom vzestupně podle primárního klíče.
  2. Když zamykáš více řádků, identifikátory nejdřív seřaď. Dotaz s FOR UPDATE musí stejné pořadí opravdu vynutit.
  3. Zamkni jen data nutná pro invariant a proveď zápis hned. HTTP volání, e-mail, soubory ani dlouhé výpočty do transakce nepatří.
  4. Ověř indexy pro podmínky UPDATE a SELECT FOR UPDATE. Zbytečně široké skeny prodlužují držení zámků a zvětšují prostor pro konflikt.
SELECT id FROM account WHERE id IN (:ids) ORDER BY id FOR UPDATE;
Oficiální PostgreSQL dokumentace k deadlockům

3. Opakuj jen bezpečně rozpoznané konflikty

  1. Zachyť jen rozpoznaný deadlock 40P01 a případně serialization_failure 40001, pokud use case používá izolaci, která s opakováním počítá.
  2. Po rollbacku vytvoř nový EntityManager a zopakuj celou transakci včetně všech čtení a kontrol. Nepokračuj od prostředního dotazu.
  3. Nastav malý limit, například tři pokusy, a mezi nimi krátký náhodný jitter. Po vyčerpání vrať řízenou chybu a zaloguj metriku.
  4. Neopakuj automaticky každou databázovou výjimku. Syntax error, porušení constraintu ani výpadek spojení nejsou potvrzený deadlock.
SQLSTATE 40P01 / 40001 → rollback → jitter → retry celé transakce
Oficiální PostgreSQL seznam SQLSTATE kódů

Krok 4

Ověř opravu pod souběhem

Sekvenční test deadlock nevyvolá. Potřebuješ dvě skutečná spojení, řízené proložení kroků a kontrolu výsledného invariantu.

  1. Reprodukuj původní opačné pořadí

    Ve dvou připojeních zamkni stejné řádky v opačném pořadí a ověř, že test před opravou zachytí SQLSTATE 40P01.

    php bin/phpunit --filter Deadlock
  2. Po opravě spusť mnoho souběžných pokusů

    Oba use case musí používat shodné pořadí. Sleduj počet deadlocků, dobu čekání a počet použitých retry.

  3. Ověř data po retry

    Operace musí proběhnout právě jednou z pohledu obchodního výsledku. Zkontroluj zůstatky, počty záznamů i unikátní klíče.

Když to zlobí

Nejčastější chyby

Deadlock se po přidání ORDER BY stále vrací

Jiný use case pravděpodobně zamyká stejné zdroje v jiném pořadí nebo dříve drží další zámek. Porovnej celé transakce, ne jen jeden dotaz.

Retry selže na zavřeném EntityManageru

Po rollbacku nepoužívej původní Unit of Work. Každý nový pokus spusť s novým EntityManagerem a znovu načti všechna data.

Aplikace opakuje i neopravitelné chyby

Filtruj podle konkrétního SQLSTATE nebo přesně mapované Doctrine výjimky. Limituj počet pokusů a ostatní chyby okamžitě předej volajícímu.

Po retry se dvakrát odeslal externí požadavek

Externí I/O bylo uvnitř opakované transakce. Přesuň ho za commit nebo použij transakční outbox a idempotentní zpracování.

Hotovo

Zamykání má teď předvídatelné pořadí.

PostgreSQL dostává krátké transakce se stejným pořadím zámků a aplikace opakuje jen přesně rozpoznané souběžné konflikty. Počet deadlocků dál sleduj jako produkční metriku.

Zavolejte mi

Zavolám vám následující pracovní den mezi 9:00 a 17:00.

Můžete mi také zavolat rovnou.

+420 605 181 728

Nechte mi telefonní číslo a pošlete žádost o zpětné zavolání.

Odesláním souhlasíte se zpracováním údajů pro vyřízení žádosti.