Praktický návod
Jak správně rezervovat skladovou zásobu při objednávce
Dostupnost ověř a změň atomicky. Dva zákazníci nesmí oba úspěšně koupit poslední kus.
Nejdřív stručně
Přečíst a potom zapsat nestačí
Když dva requesty nejdřív samostatně přečtou available = 1, oba mohou zkusit rezervovat stejný kus. Kontrola a změna proto musí proběhnout jako jedna atomická databázová operace nebo pod zámkem.
Transakce chrání rezervaci a její stav. Externí platba do ní nepatří; dlouhé čekání na zákazníka řeší samostatný rezervační záznam s expirací.
Připrav si
Co musíš definovat
Nejdřív pojmenuj význam skladových čísel a celý životní cyklus rezervace.
- Model on_hand, reserved a dostupnosti, například available = on_hand − reserved, pro konkrétní SKU a sklad.
- Stavy rezervace pending, confirmed, released a expired včetně povolených přechodů.
- Čas expirace, pravidla po úspěšné platbě, stornu a selhání procesu uprostřed.
- Idempotentní klíč objednávky a položky, aby opakovaný request nevytvořil druhou rezervaci.
Kroky 1 až 3
Rezervuj pod databázovou kontrolou
Pro jednu položku stačí podmíněný UPDATE. Pro více položek zamkni řádky vždy ve stejném pořadí a rozhodni celý košík společně.
1. Zaveď explicitní rezervaci
- Skladový řádek drž pro kombinaci SKU a skladu. Constraint zajistí on_hand >= 0, reserved >= 0 a reserved <= on_hand, pokud obchodní model nepovoluje backorder.
- Rezervaci ukládej jako samostatný záznam s order_id, sku_id, quantity, stavem a expires_at. Přidej unikátní klíč pro objednávku a SKU.
- Dostupnost počítej z autoritativní databáze. Cache nebo Elasticsearch může zobrazit informaci, ale nesmí rozhodnout poslední kus.
- Změny skladu audituj jako business události nebo ledger, aby šlo vysvětlit rezervaci, prodej, storno i ruční korekci.
available = on_hand - reserved Oficiální PostgreSQL dokumentace ke constraintům 2. Ověř a změň zásobu atomicky
- Pro jedno SKU použij podmíněný UPDATE s RETURNING. Pokud nevrátí řádek, zásoba už nestačí a rezervace nevznikne.
- Rezervační záznam a zvýšení reserved proveď v jedné transakci. Idempotentní opakování nejdřív najde existující rezervaci.
- Pro více SKU načti a zamkni všechny skladové řádky SELECT FOR UPDATE ve vzestupném pořadí ID. Po ověření všech položek teprve změny ulož.
- Během zámku nevolej platební bránu ani jiné API. Čím kratší transakce, tím menší čekání a riziko deadlocku.
UPDATE inventory
SET reserved = reserved + :qty
WHERE sku_id = :sku AND on_hand - reserved >= :qty
RETURNING on_hand, reserved; Oficiální PostgreSQL dokumentace k row lockům 3. Potvrď nebo uvolni právě jednou
- Po úspěšné platbě převeď pending na confirmed a atomicky sniž on_hand i reserved. Opakovaný callback najde confirmed a nic neodečte podruhé.
- Při stornu nebo expiraci převeď pending na released nebo expired a sniž pouze reserved. Stavová podmínka zabrání dvojímu uvolnění.
- Expirační worker vybírá malé dávky pomocí FOR UPDATE SKIP LOCKED. Více workerů tak nezpracuje stejnou rezervaci současně.
- Čas ber z databáze nebo jednotně synchronizovaných hodin a ukládej absolutní expires_at. Monitoring hlídá nejstarší pending rezervaci.
UPDATE reservation SET status = 'expired' WHERE id = :id AND status = 'pending' RETURNING quantity; Oficiální PostgreSQL dokumentace k FOR UPDATE Krok 4
Otestuj skutečný souběh
Sekvenční test overselling neodhalí. Potřebuješ dvě databázová spojení a řízený závod o stejný kus.
-
Nech dva zákazníky rezervovat poslední kus
Oba requesty spusť současně. Právě jeden uspěje, druhý dostane nedostupnost a invariant skladu zůstane platný.
php bin/phpunit --filter ConcurrentStockReservation -
Doruč potvrzení platby dvakrát
Oba callbacky vrátí konzistentní výsledek, ale on_hand a reserved se změní pouze jednou.
-
Spusť dva expirační workery
Každá pending rezervace se uvolní právě jednou a confirmed rezervace zůstane nedotčená.
Když to zlobí
Nejčastější chyby
Sklad klesl pod nulu
Kontrola a update nejsou atomické nebo chybí databázový constraint. Použij podmíněný UPDATE či row lock ve stejné transakci.
Rezervace po stornu stále blokuje kusy
Stav objednávky a uvolnění nejsou spolehlivě propojené. Zpracuj storno idempotentní událostí a sleduj stáří pending rezervací.
Expirační worker uvolnil zaplacenou objednávku
Přechod neověřil aktuální status pod zámkem. UPDATE musí mít podmínku status=pending a rozhodnutí musí být v jedné transakci.
Vícepoložkové objednávky deadlockují
Různé requesty zamykají SKU v jiném pořadí. Všechny ID předem seřaď a zámky získávej konzistentně.
Hotovo
Poslední kus může získat jen jedna objednávka.
Transakce teď atomicky chrání vznik, potvrzení i uvolnění rezervace. Dostupnost zůstává vysvětlitelná a opakované requesty nemění sklad podruhé.