sql >> Database teknologi >  >> RDS >> Mysql

PHP PDO transaktion Duplikering

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



  1. SQL ORDER BY flere kolonner

  2. Sådan administrerer du ledige værelser baseret på dages eller måneders besættelse

  3. Mysql JDBC-driver ClassNotFoundException

  4. MySQL får manglende ID'er fra tabellen