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.)