Mysql
 sql >> база данни >  >> RDS >> Mysql

PHP PDO транзакция Дублиране

Повтаряне на коментара от @GarryWelding:актуализацията на базата данни не е подходящо място в кода за обработка на описания случай на употреба. Заключването на ред в потребителската таблица не е правилното решение.

Назад на крачка назад. Изглежда, че искаме някакъв фин контрол върху покупките на потребителите. Изглежда, че имаме нужда от място, където да съхраняваме запис на потребителските покупки и тогава можем да проверим това.

Без да се гмуркам в дизайна на база данни, ще дам някои идеи тук...

В допълнение към обекта "потребител"

user
   username
   account_balance

Изглежда, че се интересуваме от информация за покупките, които потребителят е направил. Изхвърлям някои идеи за информацията/атрибутите, които може да ни интересуват, без да твърдя, че всички те са необходими за вашия случай на употреба:

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"

Не искаме да се опитваме да проследяваме цялата тази информация в „салдото на акаунта“ на потребител, особено след като може да има множество покупки от потребител.

Ако нашият случай на използване е много по-прост от този и ние само за да следим последната покупка от потребител, тогава бихме могли да запишем това в потребителския обект.

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

И след това при всяка покупка бихме могли да запишем новия акаунт_баланс и да презапишем предишната информация за „най-нова покупка“

Ако всичко, което ни интересува, е предотвратяването на множество покупки „едновременно“, трябва да дефинираме, че... означава ли това в рамките на една и съща точна микросекунда? в рамките на 10 милисекунди?

Искаме ли само да предотвратим „дублиращи“ покупки от различни компютри/сесии? Какво ще кажете за две дублиращи се заявки в една и съща сесия?

Това не как бих решил проблема. Но за да отговорим на въпроса, който зададете, ако отидем с прост случай на употреба - "предотвратете две покупки в рамките на една милисекунда една от друга" и искаме да направим това в UPDATE на user таблица

Като се има предвид дефиниция на таблица като тази:

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)

с датата и часа (до микросекундата) на най-новата покупка, записана в потребителската таблица (като се използва времето, върнато от базата данни)

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 
           )

След това можем да открием броя на редовете, засегнати от израза.

Ако получим нула засегнати редове, тогава или :user не беше намерен или :money2 е по-голямо от салдото по сметката или most_recent_purchase_dt беше в диапазона от +/- 1 милисекунда от сега. Не можем да кажем кое.

Ако са засегнати повече от нула реда, тогава знаем, че е възникнала актуализация.

РЕДАКТИРАНЕ

За да подчертая някои ключови моменти, които може да са били пренебрегнати...

Примерният SQL очаква поддръжка за части от секунди, което изисква MySQL 5.7 или по-нова версия. В 5.6 и по-стари, разделителната способност за DATETIME беше само до втората. (Обърнете внимание на дефиницията на колона в примерната таблица и SQL определя разделителна способност до микросекунда... DATETIME(6) и NOW(6) .

Примерният SQL израз очаква username да бъде ПЪРВИЧЕН КЛЮЧ или УНИКАЛЕН ключ в user маса. Това е отбелязано (но не е подчертано) в примерната дефиниция на таблицата.

Примерният SQL израз отменя актуализацията на user за два оператора, изпълнени в рамките на една милисекунда един на друг. За тестване променете тази разделителна способност от милисекунди на по-дълъг интервал. например променете го на една минута.

Тоест, променете двете поява на 1000 MICROSECOND до 60 SECOND .

Няколко други бележки:използвайте bindValue на мястото на bindParam (тъй като ние предоставяме стойности на израза, а не връщаме стойности от оператора.

Също така се уверете, че PDO е настроен да хвърля изключение, когато възникне грешка (ако няма да проверяваме връщането от PDO функциите в кода), така че кодът да не поставя своя (фигуративен) малък пръст в ъгъла на устата ни Dr.Evil стил "Просто предполагам, че всичко ще мине по план. Какво?")

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

И за да подчертая една точка, която казах по-рано, „заключването на реда“ не е правилният подход за решаване на проблема. И извършването на проверката по начина, който демонстрирах в примера, не ни казва причината, поради която покупката е била неуспешна (недостатъчни средства или в рамките на определен период от предходната покупка.)



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да показвам редовете като колони в MySQL?

  2. Нарушение на ограничението за целостта:1452 Не може да се добави или актуализира дъщерен ред:

  3. Полетата за дата и час в MySQL и лятно часово време – как да направя справка с допълнителния час?

  4. SQLSTATE[HY000] [2002] Неуспешен опит за свързване.. - При опит за свързване от локален към отдалечен сървър

  5. MySQL индексира ли NULL стойности?