sql >> Databasteknik >  >> RDS >> Mysql

PHP PDO transaktion Duplicering

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



  1. 19 Onlineresurser för att lära dig om databasdesignfel

  2. Skäl att vara optimistisk om framtiden för Microsoft Access

  3. Använda varchar(MAX) vs TEXT på SQL Server

  4. Hur kontrollerar jag om ett index finns på ett tabellfält i MySQL?