sql >> Databáze >  >> RDS >> Mysql

Duplicitní transakce PHP PDO

Ozvěna komentáře od @GarryWelding:aktualizace databáze není vhodným místem v kódu pro zpracování popsaného případu použití. Uzamčení řádku v uživatelské tabulce není to správné řešení.

Ustup o krok. Zní to, jako bychom chtěli nějakou jemnou kontrolu nad nákupy uživatelů. Zdá se, že potřebujeme místo pro ukládání záznamů o nákupech uživatelů, a pak to můžeme zkontrolovat.

Aniž bych se pouštěl do návrhu databáze, hodlám zde vyhodit pár nápadů...

Kromě entity „uživatel“

user
   username
   account_balance

Zdá se, že nás zajímají nějaké informace o nákupech, které uživatel provedl. Vyhazuji několik nápadů ohledně informací/atributů, které by nás mohly zajímat, aniž bych tvrdil, že jsou všechny potřebné pro váš případ použití:

user_purchase
   username that made the purchase
   items/services purchased
   datetime the purchase was originated
   money_amount of the purchase
   computer/session the purchase was made from
   status (completed, rejected, ...)
   reason (e.g. purchase is rejected, "insufficient funds", "duplicate item"

Nechceme se pokoušet sledovat všechny tyto informace v „zůstatku účtu“ uživatele, zejména proto, že uživatel může nakupovat vícekrát.

Pokud je náš případ použití mnohem jednodušší a my pouze sledujeme poslední nákup uživatelem, můžeme to zaznamenat do uživatelské entity.

user
  username 
  account_balance ("money")
  most_recent_purchase
     _datetime
     _item_service
     _amount ("money")
     _from_computer/session

A pak s každým nákupem bychom mohli zaznamenat nový zůstatek na účtu a přepsat předchozí informace o „posledním nákupu“

Pokud nám jde pouze o to, abychom zabránili více nákupům „současně“, musíme definovat, že... znamená to ve stejné přesné mikrosekundě? do 10 milisekund?

Chceme pouze zabránit „duplicitním“ nákupům z různých počítačů/relací? A co dva duplicitní požadavky ve stejné relaci?

Toto není jak bych problém vyřešil. Abychom však odpověděli na otázku, kterou jste položili, pokud použijeme jednoduchý případ použití – „zabránit dvěma nákupům během milisekundy od sebe“, a chceme to udělat v UPDATE user tabulka

Vzhledem k definici tabulky, jako je tato:

user
  username                 datatype    NOT NULL PRIMARY KEY 
  account_balance          datatype    NOT NULL
  most_recent_purchase_dt  DATETIME(6) NOT NULL COMMENT 'most recent purchase dt)

s datem a časem (až na mikrosekundu) posledního nákupu zaznamenaného v uživatelské tabulce (s použitím času vráceného databází)

UPDATE user u
   SET u.most_recent_purchase_dt = NOW(6) 
     , u.account_balance  = u.account_balance - :money1
 WHERE u.username         = :user
   AND u.account_balance >= :money2
   AND NOT ( u.most_recent_purchase_dt >= NOW(6) + INTERVAL -1000 MICROSECOND
         AND u.most_recent_purchase_dt <  NOW(6) + INTERVAL +1001 MICROSECOND 
           )

Poté můžeme zjistit počet řádků ovlivněných příkazem.

Pokud dostaneme nula ovlivněných řádků, pak buď :user nebyl nalezen, nebo :money2 byl vyšší než zůstatek účtu neboli most_recent_purchase_dt byl nyní v rozsahu +/- 1 milisekundy. Nemůžeme říct, které.

Pokud je ovlivněno více než nula řádků, víme, že došlo k aktualizaci.

UPRAVIT

Chcete-li zdůraznit některé klíčové body, které mohly být přehlédnuty...

Příklad SQL očekává podporu na zlomky sekund, což vyžaduje MySQL 5.7 nebo novější. Ve verzi 5.6 a dřívějších bylo rozlišení DATETIME pouze na sekundu. (Všimněte si, že definice sloupce v ukázkové tabulce a SQL specifikuje rozlišení až na mikrosekundu... DATETIME(6) a NOW(6) .

Ukázkový příkaz SQL očekává username být PRIMÁRNÍ KLÍČ nebo UNIKÁTNÍ klíč v user stůl. To je uvedeno (ale ne zvýrazněno) v definici příkladu tabulky.

Příklad příkazu SQL přepíše aktualizaci user pro dva příkazy provedené během jedné milisekundy navzájem. Pro testování změňte toto milisekundové rozlišení na delší interval. například jej změňte na jednu minutu.

To znamená, že změňte dva výskyty 1000 MICROSECOND na 60 SECOND .

Několik dalších poznámek:použijte bindValue místo bindParam (protože poskytujeme hodnoty do příkazu, nevracíme hodnoty z příkazu.

Také se ujistěte, že PDO je nastaveno tak, aby vyvolalo výjimku, když dojde k chybě (pokud nehodláme kontrolovat návrat z funkcí PDO v kódu), aby kód nedával (obrazně) malíček do rohu naše ústa Styl Dr.Evil "Předpokládám, že vše půjde podle plánu. Co?")

# enable PDO exceptions
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$sql = "
UPDATE user u
   SET u.most_recent_purchase_dt = NOW(6) 
     , u.account_balance  = u.account_balance - :money1
 WHERE u.username         = :user
   AND u.account_balance >= :money2
   AND NOT ( u.most_recent_purchase_dt >= NOW(6) + INTERVAL -60 SECOND
         AND u.most_recent_purchase_dt <  NOW(6) + INTERVAL +60 SECOND
           )";

$sth = $dbh->prepare($sql)
$sth->bindValue(':money1', $amount, PDO::PARAM_STR);
$sth->bindValue(':money2', $amount, PDO::PARAM_STR);
$sth->bindValue(':user', $user, PDO::PARAM_STR);
$sth->execute(); 

# check if row was updated, and take appropriate action
$nrows = $sth->rowCount();
if( $nrows > 0 ) {
   // row was updated, purchase successful
} else {
   // row was not updated, purchase unsuccessful
}

A abych zdůraznil bod, který jsem uvedl dříve, „uzamknout řadu“ není správný přístup k řešení problému. A provedení kontroly způsobem, který jsem ukázal v příkladu, nám neřekne důvod, proč byl nákup neúspěšný (nedostatek finančních prostředků nebo ve stanoveném časovém rámci předchozího nákupu.)



  1. Jak zkontrolovat, zda uživatel kliknul na [Storno] na InputBox ve VBA

  2. Poskytovatel OraOLEDB.Oracle není registrován na místním počítači

  3. Funkce TO_TIMESTAMP_TZ() v Oracle

  4. Oznámení MySQL 5.6 EOL