【问题标题】:PHP / MySQL - how to prevent two requests *UpdatePHP / MySQL - 如何防止两个请求 *更新
【发布时间】:2013-02-08 05:26:55
【问题描述】:

我有一个问题... 例如:用户会用他的美元买东西

  1. 查看他的美元余额
  2. 从他的账户中扣除美元
  3. 下订单 -> 订单队列
  4. 用户得到他的物品,另一个得到他的美元

假设用户在同一秒内发出 5 个请求(非常快)。 所以有可能(并且发生)5 个请求正在运行。 他只有从 1 个请求中购买的钱。现在的请求 太快了,脚本会检查他的余额,但不是很快,它会扣除 他账户里的钱。所以请求将通过两次! 如何解决?

在我开始进程之前,我在mysql中使用了LOCK:

  1. IS_FREE_LOCK - 如果没有,请检查该用户是否有锁 -> 2.
  2. GET_LOCK - 设置锁
  3. 下订单/交易
  4. 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();

【问题讨论】:

标签: php transactions


【解决方案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

【讨论】:

  • 那么如果用户设置了2个请求,事务会排队执行吗?这样当我 START TRANSACTION 时,另一个请求将在我提交或回滚时排队这么久?
  • 我认为这并不能解决 op 的问题,因为 2 次选择仍然会返回足够的美元。
  • db 中的查询不会同时执行。如果没有事务 2 选择将排队,然后更新。使用事务,您必须在跳转到下一个队列语句之前同时执行一个 BLOCK sql statemets。因此,对于事务,第二次选择将失败。
  • 第二次选择将失败并且未排队/执行?
  • ofc 将排队并执行。但是当我说失败时,我的意思是你的第二笔交易将失败,因为没有足够的美元,所以它会回滚(又名失败)。
【解决方案3】:

这就是我多年前的习惯..

results = query("UPDATE table SET value=value-5 WHERE value>=5 AND ID=1")
if (results == 1) YEY!

(这仍然是一种可靠的方法吗?)

【讨论】:

    【解决方案4】:

    您需要在 SERIALIZABLE 隔离级别使用 TRANSACTION。

    【讨论】:

      【解决方案5】:

      您需要为 MySQL UPDATE 使用数据修订。

      【讨论】:

        猜你喜欢
        • 2015-07-12
        • 1970-01-01
        • 2010-11-22
        • 1970-01-01
        • 2015-09-11
        • 2011-11-05
        • 2015-08-31
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多