Gentager kommentaren fra @GarryWelding:databaseopdateringen er ikke et passende sted i koden til at håndtere den use case, der er beskrevet. At låse en række i brugertabellen er ikke den rigtige løsning.
Tilbage et trin. Det lyder som om, vi ønsker en finmasket kontrol over brugernes køb. Det ser ud til, at vi har brug for et sted at gemme en registrering af brugerkøb, og så kan vi tjekke det.
Uden at dykke ned i et databasedesign, vil jeg smide nogle ideer ud her...
Ud over "bruger"-enheden
user
username
account_balance
Det ser ud til, at vi er interesserede i nogle oplysninger om køb, en bruger har foretaget. Jeg smider nogle ideer ud om de oplysninger/attributter, der kan være af interesse for os, uden at påstå, at disse alle er nødvendige for din brugssituation:
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 ønsker ikke at forsøge at spore alle disse oplysninger i en brugers "kontosaldo", især da der kan være flere køb fra en bruger.
Hvis vores use case er meget enklere end det, og vi kun skal holde styr på det seneste køb af en bruger, så kunne vi registrere det i brugerenheden.
user
username
account_balance ("money")
most_recent_purchase
_datetime
_item_service
_amount ("money")
_from_computer/session
Og så kunne vi med hvert køb registrere den nye kontosaldo og overskrive de tidligere "seneste køb"-oplysninger
Hvis alt, hvad vi bekymrer os om, er at forhindre flere køb "på samme tid", er vi nødt til at definere det... betyder det inden for det samme nøjagtige mikrosekund? inden for 10 millisekunder?
Ønsker vi kun at forhindre "duplikerede" køb fra forskellige computere/sessioner? Hvad med to duplikerede anmodninger på samme session?
Dette er ikke hvordan jeg ville løse problemet. Men for at besvare det spørgsmål, du stillede, hvis vi går med en simpel use case - "forhindrer to køb inden for et millisekund fra hinanden", og vi ønsker at gøre dette i en UPDATE
af user
tabel
Givet en tabeldefinition som denne:
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 dato og klokkeslæt (ned til mikrosekundet) for det seneste køb, der er registreret i brugertabellen (ved brug af klokkeslættet returneret af 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 derefter registrere antallet af rækker, der er påvirket af udsagnet.
Hvis vi får nul rækker påvirket, så enten :user
blev ikke fundet, eller :money2
var større end kontosaldoen eller most_recent_purchase_dt
var inden for et interval på +/- 1 millisekund fra nu. Vi kan ikke sige hvilken.
Hvis mere end nul rækker er berørt, ved vi, at der er sket en opdatering.
REDIGER
For at understrege nogle nøglepunkter, som måske er blevet overset...
SQL-eksemplet forventer understøttelse i brøkdele sekunder, hvilket kræver MySQL 5.7 eller nyere. I 5.6 og tidligere var DATETIME-opløsningen kun nede på den anden. (Bemærk kolonnedefinitionen i eksempeltabellen og SQL specificerer opløsning ned til mikrosekund... DATETIME(6)
og NOW(6)
.
SQL-eksemplet forventer username
at være den PRIMÆRE NØGLE eller en UNIK nøgle i user
bord. Dette er noteret (men ikke fremhævet) i eksempeltabeldefinitionen.
Eksemplet på SQL-sætningen tilsidesætter opdatering af user
for to sætninger udført inden for et millisekund af hinanden. Til test skal du ændre den millisekundsopløsning til et længere interval. for eksempel ændre det til et minut.
Det vil sige, ændre de to forekomster af 1000 MICROSECOND
til 60 SECOND
.
Et par andre bemærkninger:brug bindValue
i stedet for bindParam
(da vi leverer værdier til sætningen, ikke returnerer værdier fra sætningen.
Sørg også for, at PDO er indstillet til at kaste en undtagelse, når der opstår en fejl (hvis vi ikke skal kontrollere returneringen fra PDO-funktionerne i koden), så koden ikke sætter sin (figurative) lillefinger til hjørnet af vores mund Dr.Evil stil "Jeg går ud fra, at det hele vil gå efter planen. Hvad?")
# 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
}
Og for at understrege en pointe, jeg gjorde tidligere, er "lås rækken" ikke den rigtige tilgang til at løse problemet. Og at udføre kontrollen på den måde, jeg demonstrerede i eksemplet, fortæller os ikke, hvorfor købet var mislykket (utilstrækkelige midler eller inden for den specificerede tidsramme for det foregående køb).