PHP/MySQL - как предотвратить два запроса * Обновление
У меня есть вопрос...
Например: пользователь купит что-то за свой доллар.
- Проверьте его баланс в долларах США.
- Вычитайте USD со своего счета
- Сделать заказ → очередь заказов
- пользователь получает свой товар, а другой получает свой доллар.
Предположим, что пользователи делают 5 запросов за одну секунду (очень быстро).
Таким образом, возможно (и произойдет), что запущено 5 запросов.
У него есть только деньги, чтобы купить только с 1 запроса. Теперь запросы
так быстро, что script проверяет его баланс, но не так быстро, что он вычитает
деньги со своего счета. Поэтому запросы будут проходить два раза!
Как его решить?
Я использую LOCK в mysql перед тем, как начать процесс:
- IS_FREE_LOCK - проверка наличия блокировки для этого пользователя, если нет → 2.
- GET_LOCK - устанавливает блокировку
- сделать заказ/транзакцию
- RELEASE_LOCK - освобождает блокировку
Но это не работает. Есть ли другой способ?
function lock($id) {
mysql_query("SELECT GET_LOCK('$id', 60) AS 'GetLock'");
}
function is_free($id) {
$query = mysql_query("SELECT IS_FREE_LOCK('$id') AS 'free'");
$row = mysql_fetch_assoc($query);
if($row['free']) {
return true;
} else {
return false;
}
}
function release_lock($id) {
mysql_query("SELECT RELEASE_LOCK('$id')");
}
function account_balance($id) {
$stmt = $db->prepare("SELECT USD FROM bitcoin_user_n WHERE id = ?");
$stmt->execute(array($id));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row['USD'];
}
if(is_free(get_user_id())) {
lock(get_user_id());
if(account_balance(get_user_id()) < str2num($_POST['amount'])) {
echo "error, not enough money";
} else {
$stmt = $db->prepare("UPDATE user SET USD = USD - ? WHERE id = ?");
$stmt->execute(array(str2num($_POST['amount']), get_user_id()));
$stmt = $db->prepare("INSERT INTO offer (user_id, type, price, amount) VALUES (?, ?, ?, ?)");
$stmt->execute(array(get_user_id(), 2, str2num($_POST['amount']), 0));
}
Обновление
Протестирована транзакционная функция с помощью SELECT... FOR UPDATE
$db->beginTransaction();
$stmt = $db->prepare("SELECT value, id2 FROM test WHERE id = ? FOR UPDATE");
$stmt->execute(array(1));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if($row['value'] > 1) {
sleep(5);
$stmt = $db->prepare('UPDATE test SET value = value - 5 WHERE id = 1');
$stmt->execute();
$stmt = $db->prepare('UPDATE test SET value = value + 5 WHERE id = 2');
$stmt->execute();
echo "did have enough money";
} else {
echo "no money";
}
$db->commit();
Ответы
Ответ 1
Прежде всего, вы должны использовать транзакции, но этого недостаточно. В вашей транзакции вы можете использовать SELECT FOR UPDATE
.
В основном он говорит: "Я собираюсь обновить записи, которые я выбираю", поэтому он устанавливает те же блокировки, что и UPDATE
. Но помните, что это должно произойти внутри транзакции с отключенной автоматической записью.
Ответ 2
Используйте TRANSACTION, и если он не работает, вы можете откатиться.
Например, предположим, что текущий баланс равен $20.
Connection A Connection B
======================= ===========================
BEGIN TRANSACTION
BEGIN TRANSACTION
SELECT AccountBalance
SELECT AccountBalance
--returns $20
--sufficient balance,
--proceed with purchase
--returns $20
--sufficient balance,
--proceed with purchase
--update acquires exclusive lock
UPDATE SET AccountBalance
= AccountBalance - 20
--update blocked due
UPDATE SET AccountBalance
= AccountBalance - 20
--order complete
COMMIT TRANSACTION
--update proceeds
--database triggers
--constraint violation
--"AccountBalance >= 0"
ROLLBACK TRANSACTION
Ответ 3
Вот как я это делал много лет назад.
results = query("UPDATE table SET value=value-5 WHERE value>=5 AND ID=1")
if (results == 1) YEY!
(Является ли это еще надежным методом?)
Ответ 4
вам нужно использовать транзакцию на уровне изоляции SERIALIZABLE.
Ответ 5
Вам необходимо использовать Data revision для MySQL UPDATE.