Jag upprepar kommentaren från @GarryWelding:databasuppdateringen är inte en lämplig plats i koden för att hantera användningsfallet som beskrivs. Att låsa en rad i användartabellen är inte rätt lösning.
Backa upp ett steg. Det låter som att vi vill ha lite finkornig kontroll över användarnas köp. Det verkar som att vi behöver ett ställe att lagra ett register över användarköp, och då kan vi kontrollera det.
Utan att dyka ner i en databasdesign kommer jag att slänga ut några idéer här...
Förutom entiteten "användare"
user
username
account_balance
Det verkar som om vi är intresserade av lite information om köp en användare har gjort. Jag slänger ut några idéer om informationen/attributen som kan vara av intresse för oss, utan hävdar att dessa alla behövs för ditt användningsfall:
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"
Vi vill inte försöka spåra all den informationen i "kontosaldo" för en användare, särskilt eftersom det kan göras flera köp från en användare.
Om vårt användningsfall är mycket enklare än så, och vi bara för att hålla reda på det senaste köpet av en användare, kan vi registrera det i användarenheten.
user
username
account_balance ("money")
most_recent_purchase
_datetime
_item_service
_amount ("money")
_from_computer/session
Och sedan med varje köp kunde vi registrera det nya kontosaldot och skriva över den tidigare informationen om "senaste köpet"
Om allt vi bryr oss om är att förhindra flera köp "på samma gång", måste vi definiera det... betyder det inom samma exakta mikrosekund? inom 10 millisekunder?
Vill vi bara förhindra "dubbletter" köp från olika datorer/sessioner? Vad sägs om två dubbletter av förfrågningar på samma session?
Detta är inte hur jag skulle lösa problemet. Men för att svara på frågan du ställde, om vi går med ett enkelt användningsfall - "förhindra två köp inom en millisekund från varandra", och vi vill göra detta i en UPDATE
av user
tabell
Givet en tabelldefinition så här:
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)
med datetime (ned till mikrosekund) för det senaste köpet registrerat i användartabellen (med tiden som returneras av databasen)
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
)
Vi kan sedan upptäcka antalet rader som påverkas av påståendet.
Om vi får noll rader påverkade, då antingen :user
hittades inte, eller :money2
var större än kontosaldot, eller most_recent_purchase_dt
var inom intervallet +/- 1 millisekund nu. Vi kan inte säga vilken.
Om fler än noll rader påverkas vet vi att en uppdatering har skett.
REDIGERA
För att betona några nyckelpunkter som kan ha förbisetts...
Exemplet SQL förväntar sig stöd för bråkdelar av sekunder, vilket kräver MySQL 5.7 eller senare. I 5.6 och tidigare var DATETIME-upplösningen bara nere på tvåan. (Notera kolumndefinitionen i exempeltabellen och SQL specificerar upplösning ner till mikrosekund... DATETIME(6)
och NOW(6)
.
Exempel på SQL-satsen förväntar sig username
vara PRIMÄRNYCKEL eller UNIK nyckel i user
tabell. Detta är noterat (men inte markerat) i exempeltabelldefinitionen.
Exemplet på SQL-satsen åsidosätter uppdatering av user
för två satser som körs inom en millisekund av varandra. För testning, ändra den millisekundsupplösningen till ett längre intervall. till exempel ändra den till en minut.
Det vill säga, ändra de två förekomsterna av 1000 MICROSECOND
till 60 SECOND
.
Några andra anmärkningar:använd bindValue
i stället för bindParam
(eftersom vi tillhandahåller värden till uttalandet, inte returnerar värden från uttalandet.
Se också till att PDO är inställd på att ge ett undantag när ett fel inträffar (om vi inte ska kontrollera returen från PDO-funktionerna i koden) så att koden inte sätter sitt (bildliga) pinky finger till hörnet av vår mun Dr.Evil stil "Jag antar bara att allt kommer att gå enligt plan. Vad?")
# 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
}
Och för att betona en poäng som jag gjorde tidigare, "lås raden" är inte det rätta sättet att lösa problemet. Och att göra kontrollen på det sätt som jag visade i exemplet berättar inte varför köpet misslyckades (otillräckliga medel eller inom den specificerade tidsramen från föregående köp.)