У меня есть игровой сайт, и я хочу обновить деньги пользователей, однако, если я использую 2 компьютера в то же время, этот код будет выполняться дважды, а у пользователя останутся минус деньги. Как я могу остановить это? Это сводит меня с ума.
$db = getDB(); $sql = "UPDATE users SET money = money- :money WHERE username=:user"; $stmt = $db->prepare($sql); $stmt->bindParam(':money', $amount, PDO::PARAM_STR); $stmt->bindParam(':user', $user, PDO::PARAM_STR); $stmt->execute();
Любая помощь приветствуется.
для отрицательного баланса измените свой код на
$sql = "UPDATE users SET money = money- :money WHERE username=:user AND money >= :money";
Первая идея:
Если вы используете InnoDB, вы можете использовать транзакции для обеспечения мелкомасштабного взаимного исключения. Пример:
START TRANSACTION; UPDATE users SET money = money- :money WHERE username=:user; COMMIT;
Если вы используете MyISAM, вы можете использовать LOCK TABLE для предотвращения доступа B к таблице до тех пор, пока A не закончит внесение изменений. Пример:
LOCK TABLE t WRITE; UPDATE users SET money = money- :money WHERE username=:user;
Вторая идея:
Если обновление не работает, вы можете удалить и вставить новую строку (если у вас есть идентификатор автоматического увеличения, дубликатов не будет).
Повторяя комментарий от @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 (eg purchase is rejected, "insufficient funds", "duplicate item"
Мы не хотим пытаться отслеживать всю эту информацию в «балансе счета» пользователя, тем более, что у пользователя может быть несколько покупок.
Если наш пример использования намного проще, и мы только отслеживаем самую последнюю покупку пользователя, тогда мы можем записать это в пользовательском объекте.
user username account_balance ("money") most_recent_purchase _datetime _item_service _amount ("money") _from_computer/session
И затем с каждой покупкой мы могли бы записать новый account_balance и перезаписать предыдущую информацию о «последней покупке»
Если все, о чем мы заботимся, это предотвращение нескольких покупок «в то же время», нам нужно определить, что … означает ли это в пределах одной и той же точной микросекунды? в течение 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 )
Затем мы можем определить количество строк, на которые влияет оператор.
Если мы :money2
нулевыми строками, то либо :user
не был найден, либо :money2
был больше, чем остаток на счете, или most_recent_purchase_dt
находился в диапазоне от +/- 1 миллисекунды теперь. Мы не можем сказать, что.
Если затронуты более чем нулевые строки, мы знаем, что произошло обновление.
РЕДАКТИРОВАТЬ
Чтобы подчеркнуть некоторые ключевые моменты, которые могли быть упущены …
Пример SQL ожидает поддержки дробных секунд, для чего требуется MySQL 5.7 или новее. В 5.6 и ранее разрешение DATETIME было только до второго. (Обратите внимание на определение столбца в таблице примеров, а SQL указывает разрешение до микросекунды … DATETIME(6)
и NOW(6)
.
Пример SQL-оператора ожидает, что username
будет PRIMARY KEY или UNIQUE-ключ в таблице user
. Это отмечено (но не выделено) в примере таблицы.
Пример инструкции SQL переопределяет обновление user
для двух операторов, выполняемых в течение одной миллисекунды друг друга. Для тестирования измените разрешение миллисекунды на более длительный интервал. например, изменить его на одну минуту.
То есть, измените два появления 1000 MICROSECOND
на 60 SECOND
.
Несколько других примечаний: используйте bindValue
вместо bindParam
(поскольку мы предоставляем значения в инструкции, а не возвращаем значения из инструкции.
Также убедитесь, что PDO настроен на создание исключения при возникновении ошибки (если мы не будем проверять возврат из функций PDO в коде), поэтому код не помещает его (фигуративный) мизинец в угол наш рот д-р Эвил стиль «Я просто предполагаю, что все пойдет по плану. Что?»)
# 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 }
И чтобы подчеркнуть то, что я сделал ранее, «заблокировать строку» – это не правильный подход к решению проблемы. И делая проверку, как я продемонстрировал в примере, не говорит нам, почему покупка была неудачной (недостаточные средства или в определенные сроки предшествующей покупки).