In Anlehnung an den Kommentar von @GarryWelding:Die Datenbankaktualisierung ist kein geeigneter Ort im Code, um den beschriebenen Anwendungsfall zu behandeln. Das Sperren einer Zeile in der Benutzertabelle ist nicht die richtige Lösung.
Gehen Sie einen Schritt zurück. Es hört sich so an, als wollten wir eine feinkörnige Kontrolle über Benutzerkäufe. Anscheinend brauchen wir einen Ort, an dem wir eine Aufzeichnung der Benutzerkäufe speichern können, und dann können wir das überprüfen.
Ohne mich in ein Datenbankdesign zu vertiefen, werde ich hier einige Ideen vorstellen...
Zusätzlich zur Entität "Benutzer"
user
username
account_balance
Scheint, als wären wir an einigen Informationen über Käufe interessiert, die ein Benutzer getätigt hat. Ich werfe einige Ideen zu den Informationen/Attributen heraus, die für uns von Interesse sein könnten, ohne zu behaupten, dass diese alle für Ihren Anwendungsfall benötigt werden:
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"
Wir möchten nicht versuchen, all diese Informationen im "Kontostand" eines Benutzers nachzuverfolgen, insbesondere da ein Benutzer mehrere Käufe tätigen kann.
Wenn unser Anwendungsfall viel einfacher ist und wir nur den letzten Kauf eines Benutzers verfolgen möchten, könnten wir dies in der Benutzerentität aufzeichnen.
user
username
account_balance ("money")
most_recent_purchase
_datetime
_item_service
_amount ("money")
_from_computer/session
Und dann könnten wir bei jedem Kauf den neuen account_balance aufzeichnen und die vorherigen Informationen zum "neuesten Kauf" überschreiben
Wenn es uns nur darum geht, mehrere Käufe "gleichzeitig" zu verhindern, müssen wir das definieren ... bedeutet das innerhalb genau derselben Mikrosekunde? innerhalb von 10 Millisekunden?
Wollen wir nur "doppelte" Käufe von verschiedenen Computern/Sitzungen verhindern? Was ist mit zwei doppelten Anfragen in derselben Sitzung?
Das ist nicht wie ich das Problem lösen würde. Aber um die Frage zu beantworten, die Sie gestellt haben, wenn wir uns für einen einfachen Anwendungsfall entscheiden – „zwei Käufe innerhalb einer Millisekunde voneinander verhindern“, und wir dies in einem UPDATE
tun möchten von user
Tabelle
Bei einer Tabellendefinition wie dieser:
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)
mit Datum und Uhrzeit (auf die Mikrosekunde genau) des letzten in der Benutzertabelle aufgezeichneten Kaufs (unter Verwendung der von der Datenbank zurückgegebenen Zeit)
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
)
Wir können dann die Anzahl der von der Anweisung betroffenen Zeilen erkennen.
Wenn null Zeilen betroffen sind, dann entweder :user
wurde nicht gefunden oder :money2
höher war als der Kontostand oder most_recent_purchase_dt
lag in einem Bereich von +/- 1 Millisekunde von jetzt. Wir können nicht sagen, welche.
Wenn mehr als null Zeilen betroffen sind, wissen wir, dass eine Aktualisierung stattgefunden hat.
BEARBEITEN
Um einige wichtige Punkte hervorzuheben, die möglicherweise übersehen wurden...
Das Beispiel-SQL erwartet Unterstützung für Bruchteile von Sekunden, wofür MySQL 5.7 oder höher erforderlich ist. In 5.6 und früher war die DATETIME-Auflösung nur auf die Sekunde genau. (Beachten Sie die Spaltendefinition in der Beispieltabelle und SQL spezifiziert die Auflösung bis auf Mikrosekunden... DATETIME(6)
und NOW(6)
.
Die Beispiel-SQL-Anweisung erwartet username
der PRIMARY KEY oder ein UNIQUE-Schlüssel im user
sein Tisch. Dies ist in der Beispieltabellendefinition vermerkt (aber nicht hervorgehoben).
Die Beispiel-SQL-Anweisung überschreibt die Aktualisierung von user
für zwei Anweisungen, die innerhalb von einer Millisekunde ausgeführt werden von einander. Ändern Sie zum Testen diese Millisekundenauflösung in ein längeres Intervall. Ändern Sie ihn beispielsweise in eine Minute.
Das heißt, ändern Sie die beiden Vorkommen von 1000 MICROSECOND
bis 60 SECOND
.
Ein paar andere Anmerkungen:Verwenden Sie bindValue
anstelle von bindParam
(da wir Werte für die Anweisung bereitstellen und keine Werte aus der Anweisung zurückgeben.
Stellen Sie außerdem sicher, dass PDO so eingestellt ist, dass es eine Ausnahme auslöst, wenn ein Fehler auftritt (wenn wir die Rückgabe von den PDO-Funktionen im Code nicht überprüfen), damit der Code seinen (bildlichen) kleinen Finger nicht in die Ecke von setzt unser Mund im Dr.Evil-Stil "Ich gehe einfach davon aus, dass alles nach Plan läuft. Was?")
# 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
}
Und um einen Punkt zu betonen, den ich zuvor gemacht habe, ist "Lock the Row" nicht der richtige Ansatz, um das Problem zu lösen. Und wenn Sie die Überprüfung so durchführen, wie ich es im Beispiel gezeigt habe, erfahren Sie nicht, warum der Kauf nicht erfolgreich war (unzureichende Deckung oder innerhalb des angegebenen Zeitrahmens des vorherigen Kaufs).