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.
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í
- 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.
- 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.
- 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.
- 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í
- 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.
- Když zamykáš více řádků, identifikátory nejdřív seřaď. Dotaz s FOR UPDATE musí stejné pořadí opravdu vynutit.
- 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ří.
- 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
- Zachyť jen rozpoznaný deadlock 40P01 a případně serialization_failure 40001, pokud use case používá izolaci, která s opakováním počítá.
- Po rollbacku vytvoř nový EntityManager a zopakuj celou transakci včetně všech čtení a kontrol. Nepokračuj od prostředního dotazu.
- 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.
- 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.
-
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 -
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.
-
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.