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

Transazione PHP PDO Duplicazione

Facendo eco al commento di @GarryWelding:l'aggiornamento del database non è un posto appropriato nel codice per gestire il caso d'uso descritto. Bloccare una riga nella tabella utente non è la soluzione giusta.

Eseguire il backup di un passaggio. Sembra che vogliamo un controllo granulare sugli acquisti degli utenti. Sembra che abbiamo bisogno di un posto dove archiviare un record degli acquisti degli utenti e quindi possiamo verificarlo.

Senza immergermi nella progettazione di un database, ho intenzione di lanciare alcune idee qui...

Oltre all'entità "utente"

user
   username
   account_balance

Sembra che siamo interessati ad alcune informazioni sugli acquisti effettuati da un utente. Sto lanciando alcune idee sulle informazioni/attributi che potrebbero interessarci, senza affermare che sono tutti necessari per il tuo caso d'uso:

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"

Non vogliamo provare a tenere traccia di tutte queste informazioni nel "saldo dell'account" di un utente, soprattutto perché possono esserci più acquisti da un utente.

Se il nostro caso d'uso è molto più semplice di quello, e dobbiamo solo tenere traccia dell'acquisto più recente da parte di un utente, potremmo registrarlo nell'entità utente.

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

E poi, ad ogni acquisto, potremmo registrare il nuovo account_balance e sovrascrivere le informazioni precedenti sull'"acquisto più recente"

Se tutto ciò che ci interessa è prevenire più acquisti "contemporaneamente", dobbiamo definire che... significa entro lo stesso esatto microsecondo? entro 10 millisecondi?

Vogliamo solo impedire acquisti "duplicati" da computer/sessioni differenti? Che ne dici di due richieste duplicate nella stessa sessione?

Questo non come risolverei il problema. Ma per rispondere alla domanda che hai posto, se andiamo con un semplice caso d'uso:"impedire due acquisti entro un millisecondo l'uno dall'altro", e vogliamo farlo in un UPDATE di user tabella

Data una definizione di tabella come questa:

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)

con la data e ora (fino al microsecondo) dell'ultimo acquisto registrato nella tabella utenti (utilizzando l'ora restituita dal database)

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 
           )

Possiamo quindi rilevare il numero di righe interessate dall'istruzione.

Se otteniamo zero righe interessate, allora o :user non è stato trovato o :money2 era maggiore del saldo del conto o most_recent_purchase_dt era entro un intervallo di +/- 1 millisecondo al momento. Non possiamo dire quale.

Se sono interessate più di zero righe, allora sappiamo che si è verificato un aggiornamento.

MODIFICA

Per sottolineare alcuni punti chiave che potrebbero essere stati trascurati...

L'esempio SQL prevede il supporto per una frazione di secondo, che richiede MySQL 5.7 o successivo. In 5.6 e precedenti, la risoluzione DATETIME era ridotta solo al secondo. (Nota la definizione della colonna nella tabella di esempio e SQL specifica la risoluzione fino al microsecondo... DATETIME(6) e NOW(6) .

L'istruzione SQL di esempio prevede username essere la CHIAVE PRIMARIA o una chiave UNICA nel user tavolo. Questo è notato (ma non evidenziato) nella definizione della tabella di esempio.

L'istruzione SQL di esempio sovrascrive l'aggiornamento di user per due istruzioni eseguite entro un millisecondo di ciascun altro. Per il test, cambia la risoluzione in millisecondi con un intervallo più lungo. ad esempio, cambialo in un minuto.

Cioè, cambia le due occorrenze di 1000 MICROSECOND a 60 SECOND .

Qualche altra nota:usa bindValue al posto di bindParam (dal momento che stiamo fornendo valori all'istruzione, non restituendo valori dall'istruzione.

Assicurati anche che PDO sia impostato per generare un'eccezione quando si verifica un errore (se non controlleremo il ritorno dalle funzioni PDO nel codice) in modo che il codice non metta il suo mignolo (figurativo) nell'angolo di la nostra bocca in stile Dr.Evil "Presumo solo che andrà tutto secondo i piani. Cosa?")

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

E per sottolineare un punto che ho sottolineato in precedenza, "bloccare la riga" non è l'approccio giusto per risolvere il problema. E facendo il controllo come ho mostrato nell'esempio, non ci dice il motivo per cui l'acquisto non è andato a buon fine (fondi insufficienti o entro il periodo di tempo specificato dall'acquisto precedente.)