Mysql
 sql >> Datenbank >  >> RDS >> Mysql

PHP-PDO-Transaktion Duplizieren

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