Повтаряне на коментара от @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
}
И за да подчертая една точка, която казах по-рано, „заключването на реда“ не е правилният подход за решаване на проблема. И извършването на проверката по начина, който демонстрирах в примера, не ни казва причината, поради която покупката е била неуспешна (недостатъчни средства или в рамките на определен период от предходната покупка.)