Mysql
 sql >> Baza danych >  >> RDS >> Mysql

Transakcja PHP PDO Duplikacja

Powtórzenie komentarza z @GarryWelding:aktualizacja bazy danych nie jest odpowiednim miejscem w kodzie do obsługi opisanego przypadku użycia. Zablokowanie wiersza w tabeli użytkowników nie jest dobrym rozwiązaniem.

Cofnij się o krok. Wygląda na to, że chcemy mieć szczegółową kontrolę nad zakupami użytkowników. Wygląda na to, że potrzebujemy miejsca do przechowywania rejestru zakupów użytkowników, a następnie możemy to sprawdzić.

Bez zagłębiania się w projektowanie bazy danych, przedstawię tutaj kilka pomysłów...

Oprócz jednostki „użytkownik”

user
   username
   account_balance

Wygląda na to, że interesują nas informacje o zakupach dokonanych przez użytkownika. Przedstawiam kilka pomysłów na temat informacji/atrybutów, które mogą nas zainteresować, nie twierdząc, że są one potrzebne w Twoim przypadku użycia:

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"

Nie chcemy próbować śledzić wszystkich tych informacji w „saldzie konta” użytkownika, zwłaszcza że użytkownik może dokonać wielu zakupów.

Jeśli nasz przypadek użycia jest znacznie prostszy i śledzimy tylko najnowszy zakup dokonany przez użytkownika, możemy to zarejestrować w encji użytkownika.

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

A potem przy każdym zakupie moglibyśmy zarejestrować nowe saldo konta i nadpisać poprzednie informacje o „najnowszym zakupie”

Jeśli wszystko, na czym nam zależy, to zapobieganie wielokrotnym zakupom „w tym samym czasie”, musimy to zdefiniować… czy to oznacza dokładnie tę samą mikrosekundę? w ciągu 10 milisekund?

Czy chcemy tylko zapobiec „zduplikowanym” zakupom z różnych komputerów/sesji? A co z dwoma zduplikowanymi żądaniami w tej samej sesji?

To nie jak rozwiązałbym problem. Ale odpowiadając na pytanie, które zadałeś, jeśli pójdziemy z prostym przypadkiem użycia - "zapobiegnij dwóm zakupom w ciągu milisekundy od siebie", i chcemy to zrobić w UPDATE user tabela

Mając taką definicję tabeli:

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)

z datą i godziną (z dokładnością do mikrosekundy) ostatniego zakupu zapisanego w tabeli użytkownika (przy użyciu czasu zwróconego przez bazę danych)

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 
           )

Możemy wtedy wykryć liczbę wierszy, których dotyczy instrukcja.

Jeśli dostaniemy zero wierszy, wtedy albo :user nie znaleziono lub :money2 była większa niż saldo konta, czyli most_recent_purchase_dt znajdował się w zakresie +/- 1 milisekundy. Nie możemy powiedzieć, które.

Jeśli dotyczy to więcej niż zera wierszy, wiemy, że nastąpiła aktualizacja.

EDYTUJ

Aby podkreślić niektóre kluczowe punkty, które mogły zostać przeoczone...

Przykładowy SQL oczekuje obsługi ułamków sekund, co wymaga MySQL w wersji 5.7 lub nowszej. W wersji 5.6 i wcześniejszych rozdzielczość DATETIME spadła tylko do sekundy. (Zwróć uwagę na definicję kolumny w przykładowej tabeli, a SQL określa rozdzielczość z dokładnością do mikrosekundy... DATETIME(6) i NOW(6) .

Przykładowa instrukcja SQL oczekuje username być KLUCZEM PODSTAWOWYM lub UNIKATOWYM w user stół. Jest to odnotowane (ale nie wyróżnione) w przykładowej definicji tabeli.

Przykładowa instrukcja SQL zastępuje aktualizację user dla dwóch instrukcji wykonanych w ciągu jednej milisekundy siebie nawzajem. Na potrzeby testowania zmień tę rozdzielczość milisekundową na dłuższy interwał. na przykład zmień go na jedną minutę.

Oznacza to, że zmień dwa wystąpienia 1000 MICROSECOND do 60 SECOND .

Kilka innych uwag:użyj bindValue zamiast bindParam (ponieważ dostarczamy wartości do instrukcji, a nie zwracamy wartości z instrukcji.

Upewnij się również, że PDO jest ustawione tak, aby rzucało wyjątek, gdy wystąpi błąd (jeśli nie zamierzamy sprawdzać powrotu z funkcji PDO w kodzie), aby kod nie umieszczał swojego (obrazowego) małego palca w rogu nasze usta w stylu Dr.Evil „Po prostu zakładam, że wszystko pójdzie zgodnie z planem. Co?”)

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

A żeby podkreślić moją wcześniejszą uwagę, „zamknij rząd” nie jest właściwym podejściem do rozwiązania problemu. A wykonanie czeku w sposób, który zademonstrowałem w przykładzie, nie mówi nam, dlaczego zakup się nie powiódł (brak wystarczających środków lub w określonym przedziale czasowym od poprzedniego zakupu).



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak używać MySQL z Deno i Oak?

  2. MySQL - zapytanie UPDATE za pomocą instrukcji SET zależnej od wyniku poprzedniej instrukcji SET

  3. Flask-Sqlalchemy Brakujący BEGIN wydaje się powodować brak synchronizacji sesji

  4. Pobieranie nieprzetworzonego ciągu zapytania SQL z przygotowanych instrukcji PDO

  5. Nie można połączyć się z serwerem MySQL na „localhost” (10061)