sql >> Database >  >> RDS >> Mysql

PHP PDO transactie Dupliceren

In navolging van de opmerking van @GarryWelding:de database-update is geen geschikte plaats in de code om de beschreven use case af te handelen. Een rij vergrendelen in de gebruikerstabel is niet de juiste oplossing.

Een stap achteruit. Het klinkt alsof we een fijnmazige controle willen over gebruikersaankopen. Het lijkt erop dat we een plaats nodig hebben om een ​​overzicht van gebruikersaankopen op te slaan, en dan kunnen we dat controleren.

Zonder in een database-ontwerp te duiken, ga ik hier wat ideeën naar voren brengen...

Naast de entiteit "gebruiker"

user
   username
   account_balance

Het lijkt erop dat we geïnteresseerd zijn in informatie over aankopen die een gebruiker heeft gedaan. Ik gooi wat ideeën naar voren over de informatie/attributen die voor ons interessant kunnen zijn, zonder te beweren dat deze allemaal nodig zijn voor uw gebruik:

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"

We willen niet proberen al die informatie bij te houden in het 'rekeningsaldo' van een gebruiker, vooral omdat er meerdere aankopen van een gebruiker kunnen zijn.

Als onze use case veel eenvoudiger is dan dat, en we alleen de meest recente aankoop door een gebruiker bijhouden, dan kunnen we dat vastleggen in de gebruikerstiteit.

user
  username 
  account_balance ("money")
  most_recent_purchase
     _datetime
     _item_service
     _amount ("money")
     _from_computer/session

En dan konden we bij elke aankoop het nieuwe account_saldo registreren en de vorige informatie over "meest recente aankoop" overschrijven

Als het ons alleen maar gaat om het voorkomen van meerdere aankopen "tegelijkertijd", moeten we dat definiëren... betekent dat binnen dezelfde exacte microseconde? binnen 10 milliseconden?

Willen we alleen "dubbele" aankopen van verschillende computers/sessies voorkomen? Hoe zit het met twee dubbele verzoeken op dezelfde sessie?

Dit is niet hoe ik het probleem zou oplossen. Maar om de door u gestelde vraag te beantwoorden, als we gaan voor een eenvoudige use-case - "voorkom twee aankopen binnen een milliseconde van elkaar", en we willen dit doen in een UPDATE van user tafel

Gegeven een tabeldefinitie als deze:

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)

met de datetime (tot op de microseconde) van de meest recente aankoop vastgelegd in de gebruikerstabel (gebruikmakend van de tijd die door de database wordt geretourneerd)

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 
           )

We kunnen dan het aantal rijen detecteren dat door de instructie wordt beïnvloed.

Als we nul rijen krijgen, dan is ofwel :user niet gevonden, of :money2 was groter dan het rekeningsaldo, of most_recent_purchase_dt was binnen een bereik van +/- 1 milliseconde van nu. We kunnen niet zeggen welke.

Als het meer dan nul rijen betreft, weten we dat er een update heeft plaatsgevonden.

BEWERKEN

Om enkele belangrijke punten te benadrukken die mogelijk over het hoofd zijn gezien...

De voorbeeld-SQL verwacht ondersteuning voor fractionele seconden, waarvoor MySQL 5.7 of hoger vereist is. In 5.6 en eerder was de DATETIME-resolutie slechts tot op de seconde nauwkeurig. (Let op de kolomdefinitie in de voorbeeldtabel en SQL specificeert resolutie tot microseconde... DATETIME(6) en NOW(6) .

De voorbeeld-SQL-instructie verwacht username om de PRIMAIRE SLEUTEL te zijn of een UNIEKE sleutel in de user tafel. Dit wordt opgemerkt (maar niet gemarkeerd) in de voorbeeldtabeldefinitie.

De voorbeeld-SQL-instructie overschrijft de update van user voor twee opdrachten uitgevoerd binnen één milliseconde van elkaar. Wijzig voor het testen die milliseconde resolutie in een langer interval. verander het bijvoorbeeld in één minuut.

Dat wil zeggen, verander de twee exemplaren van 1000 MICROSECOND tot 60 SECOND .

Een paar andere opmerkingen:gebruik bindValue in plaats van bindParam (aangezien we waarden aan de instructie geven en geen waarden van de instructie retourneren.

Zorg er ook voor dat PDO is ingesteld om een ​​uitzondering te genereren wanneer er een fout optreedt (als we de terugkeer van de PDO-functies in de code niet gaan controleren), zodat de code zijn (figuurlijke) pink niet naar de hoek van de onze mond Dr.Evil-stijl "Ik neem aan dat het allemaal volgens plan zal verlopen. Wat?")

# 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
}

En om een ​​punt dat ik eerder maakte te benadrukken:"lock the row" is niet de juiste benadering om het probleem op te lossen. En als we de controle uitvoeren zoals ik in het voorbeeld heb laten zien, vertellen we ons niet waarom de aankoop niet succesvol was (onvoldoende geld of binnen de gespecificeerde tijdspanne van de vorige aankoop.)



  1. Heroku pg:pull kan schema niet invullen

  2. Hoe items recursief uit de tabel te verwijderen?

  3. Tijdsverschil tussen 2 datums in minuten berekenen

  4. Zoeken naar geavanceerd php/mysql pagineringscript