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.

30 minut · E-shop a PostgreSQL

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

  1. Skladový řádek drž pro kombinaci SKU a skladu. Constraint zajistí on_hand >= 0, reserved >= 0 a reserved <= on_hand, pokud obchodní model nepovoluje backorder.
  2. 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.
  3. Dostupnost počítej z autoritativní databáze. Cache nebo Elasticsearch může zobrazit informaci, ale nesmí rozhodnout poslední kus.
  4. 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

  1. Pro jedno SKU použij podmíněný UPDATE s RETURNING. Pokud nevrátí řádek, zásoba už nestačí a rezervace nevznikne.
  2. Rezervační záznam a zvýšení reserved proveď v jedné transakci. Idempotentní opakování nejdřív najde existující rezervaci.
  3. 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ž.
  4. 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

  1. 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é.
  2. 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í.
  3. Expirační worker vybírá malé dávky pomocí FOR UPDATE SKIP LOCKED. Více workerů tak nezpracuje stejnou rezervaci současně.
  4. Č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.

  1. 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
  2. Doruč potvrzení platby dvakrát

    Oba callbacky vrátí konzistentní výsledek, ale on_hand a reserved se změní pouze jednou.

  3. 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é.

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.