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

У меня есть игровой сайт, и я хочу обновить деньги пользователей, однако, если я использую 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(); 

Любая помощь приветствуется.

Solutions Collecting From Web of "PHP PDO транзакция Дублирование"

для отрицательного баланса измените свой код на

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

И чтобы подчеркнуть то, что я сделал ранее, «заблокировать строку» – это не правильный подход к решению проблемы. И делая проверку, как я продемонстрировал в примере, не говорит нам, почему покупка была неудачной (недостаточные средства или в определенные сроки предшествующей покупки).