Faisant écho au commentaire de @GarryWelding :la mise à jour de la base de données n'est pas un endroit approprié dans le code pour gérer le cas d'utilisation décrit. Verrouiller une ligne dans la table des utilisateurs n'est pas la bonne solution.
Reculez d'un pas. Il semble que nous voulions un contrôle précis sur les achats des utilisateurs. Il semble que nous ayons besoin d'un endroit pour stocker un enregistrement des achats des utilisateurs, et ensuite nous pourrons vérifier cela.
Sans plonger dans la conception d'une base de données, je vais lancer quelques idées ici...
En plus de l'entité "utilisateur"
user
username
account_balance
Il semble que nous soyons intéressés par certaines informations sur les achats effectués par un utilisateur. Je lance quelques idées sur les informations/attributs qui pourraient nous intéresser, sans prétendre qu'ils sont tous nécessaires pour votre cas d'utilisation :
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"
Nous ne voulons pas essayer de suivre toutes ces informations dans le "solde du compte" d'un utilisateur, d'autant plus qu'il peut y avoir plusieurs achats d'un utilisateur.
Si notre cas d'utilisation est beaucoup plus simple que cela et que nous ne voulons garder une trace que de l'achat le plus récent d'un utilisateur, nous pourrions alors l'enregistrer dans l'entité utilisateur.
user
username
account_balance ("money")
most_recent_purchase
_datetime
_item_service
_amount ("money")
_from_computer/session
Et ensuite, à chaque achat, nous pourrions enregistrer le nouveau account_balance et écraser les informations précédentes sur "l'achat le plus récent"
Si tout ce qui nous intéresse, c'est d'empêcher plusieurs achats "en même temps", nous devons définir cela... cela signifie-t-il dans la même microseconde exacte ? en moins de 10 millisecondes ?
Voulons-nous seulement empêcher les achats « dupliqués » à partir de différents ordinateurs/sessions ? Qu'en est-il de deux demandes en double sur la même session ?
Ce n'est pas comment je résoudrais le problème. Mais pour répondre à la question que vous avez posée, si nous partons d'un cas d'utilisation simple - "empêcher deux achats à moins d'une milliseconde l'un de l'autre", et nous voulons le faire dans une UPDATE
de user
tableau
Étant donné une définition de table comme celle-ci :
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)
avec la date et l'heure (à la microseconde près) du dernier achat enregistré dans la table des utilisateurs (en utilisant l'heure renvoyée par la base de données)
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
)
On peut alors détecter le nombre de lignes concernées par l'instruction.
Si aucune ligne n'est affectée, alors soit :user
n'a pas été trouvé, ou :money2
était supérieur au solde du compte, ou most_recent_purchase_dt
était dans une plage de +/- 1 milliseconde de maintenant. Nous ne pouvons pas dire lequel.
Si plus de zéro ligne est affectée, nous savons qu'une mise à jour a eu lieu.
MODIFIER
Pour souligner quelques points clés qui auraient pu être négligés...
L'exemple SQL attend la prise en charge des fractions de seconde, ce qui nécessite MySQL 5.7 ou version ultérieure. Dans 5.6 et versions antérieures, la résolution DATETIME n'était qu'à la seconde près. (Notez la définition de la colonne dans l'exemple de table et SQL spécifie la résolution jusqu'à la microseconde... DATETIME(6)
et NOW(6)
.
L'exemple d'instruction SQL attend username
être la CLÉ PRIMAIRE ou une clé UNIQUE dans le user
table. Ceci est noté (mais pas mis en évidence) dans l'exemple de définition de table.
L'exemple d'instruction SQL remplace la mise à jour de user
pour deux instructions exécutées en une milliseconde les uns des autres. Pour les tests, remplacez cette résolution en millisecondes par un intervalle plus long. par exemple, changez-le en une minute.
C'est-à-dire, changez les deux occurrences de 1000 MICROSECOND
à 60 SECOND
.
Quelques autres remarques :utilisez bindValue
à la place de bindParam
(puisque nous fournissons des valeurs à l'instruction, nous ne renvoyons pas les valeurs de l'instruction.
Assurez-vous également que PDO est configuré pour lancer une exception lorsqu'une erreur se produit (si nous n'allons pas vérifier le retour des fonctions PDO dans le code) afin que le code ne mette pas son petit doigt (figuratif) au coin de notre bouche Dr.Evil style "Je suppose juste que tout se passera comme prévu. Quoi?")
# 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
}
Et pour souligner un point que j'ai fait plus tôt, "verrouiller la ligne" n'est pas la bonne approche pour résoudre le problème. Et faire la vérification comme je l'ai démontré dans l'exemple, ne nous dit pas la raison pour laquelle l'achat a échoué (fonds insuffisants ou dans le délai spécifié de l'achat précédent.)