SQL阻止同时选择同一个字段

时间:2016-10-28 04:01:17

标签: php mysqli

我想获得最后的余额并从后端更新xxx用户的一些交易。 不幸的是,同时,xxx也从前端执行事务,所以当我处理我的查询时,xxx也处理相同的查询,所以它得到相同的最后余额。

这是我的剧本。
假设:xxx上次余额为10000

$transaction = 1000;
$getData = mysqli_fetch_array(mysqli_query($conn,"select balance from tableA where user='xxx'"));
$balance = $getData["balance"] - $transaction; //10000 - 1000 = 9000
mysqli_query($conn,"update tableA set balance='".$balance."' where user='xxx'");

同时用户xxx从前端做交易..

$transaction = 500;
$getData = mysqli_fetch_array(mysqli_query($conn,"select balance from tableA where user='xxx'"));
$balance = $getData["balance"] - $transaction; //10000-500 it should be 9000-500
mysqli_query($conn,"update tableA set balance='".$balance."' where user='xxx'");

如何首先完成查询,然后用户xxx可以处理查询?

6 个答案:

答案 0 :(得分:3)

您可以使用TableA LOCK TABLES 命令锁定表格“MySQL”。

这是逻辑流程:

  • LOCK TABLES "TableA" WRITE;

  • 执行您的第一个查询

然后

  • UNLOCK TABLES;

请参阅:

http://dev.mysql.com/doc/refman/5.5/en/lock-tables.html

答案 1 :(得分:2)

使用InoBD引擎和事务使其成为ACID(https://en.wikipedia.org/wiki/ACID

mysqli_begin_transaction($conn);
...
mysqli_commit($conn)

另外,为什么不使用查询来增加余额

mysqli_query($conn,"update tableA set balance= balance + '".$transaction."' where user='xxx'");

答案 2 :(得分:2)

这是可用的方法之一。

你必须为你的桌子使用InnoDB引擎。 InnoDB支持行锁,因此您不需要锁定整个表来仅更新一个与给定用户相关的ROW。 (表锁定将阻止执行其他INSERT/UPDATE/DELETE操作,导致他们必须等待释放此表LOCK。

在InnoDB中,当您使用ROW LOCK执行SELECT查询时,您可以实现FOR UPDATE。 (但在此你必须使用事务来实现LOCK)。当你SELECT ... FOR UPDATE时 在事务中,mysql会锁定您选择的给定行,直到提交事务为止。 并且假设您在后端为用户输入XXX进行SELECT ... FOR UPDATE查询,同时前端对同一个XXX进行相同的查询。 执行的第一个查询(来自后端)将锁定数据库中的条目,第二个查询将等待第一个查询完成, 这可能会导致前端请求完成延迟。

但是要使这种情况起作用,您必须同时放置前端和后端查询 在交易中,两个SELECT查询最终都必须FOR UPDATE

所以你的代码看起来像这样:

$transaction = 1000;

mysqli_begin_transaction($conn);

$getData = mysqli_fetch_array(mysqli_query($conn,"SELECT balance FROM tableA WHERE user='xxx' FOR UPDATE"));
$balance = $getData["balance"] - $transaction; //10000 - 1000 = 9000
mysqli_query($conn,"UPDATE tableA SET balance='".$balance."' WHERE user='xxx'");

mysqli_commit($conn);

如果这是你的后端代码,前端代码看起来应该非常相似 - 开始/提交事务+ FOR UPDATE

关于FOR UPDATE的最好的事情之一是,如果您需要查询LOCK某行并使用此数据进行一些计算  在给定方案中,但同时您需要选择同一行的其他查询,并且需要该行中的最新数据,  你可以简单地完成这个没有事务的查询,最后没有FOR UPDATE。因此,您将有LOCKED行和正在阅读的其他正常SELECTs(当然,他们会读取LOCK开始之前存储的旧信息。

答案 3 :(得分:0)

基本上有两种方法可以解决这个问题:

  • 锁定桌子。
  • 使用交易。

在这种情况下最常见的是使用事务,以确保您执行的所有操作都是原子操作。这意味着如果一个步骤失败,则所有内容都会在更改开始之前回滚。

通常情况下,人们也会在查询中执行操作,这很简单。因为数据库引擎能够进行简单的计算。在这种情况下,您可能需要检查用户是否确实对其帐户有足够的信用,这反过来表明您需要检查。
在您减去金额之后,我只是将支票移动到保险柜上。 (防止比赛条件等)

一个让您入门的简单示例:     

$conn = new mysqli();

/**
 * Updates the user's credit with the amount specified.
 * 
 * Returns false if the resulting amount is less than 0.
 * Exceptions are thrown in case of SQL errors.
 *  
 * @param mysqli $conn
 * @param int $userID
 * @param int $amount
 * @throws Exception
 * @return boolean
 */
function update_credit (mysqli $conn, $userID, $amount) {
    // Using transaction so that we can roll back in case of errors.
    $conn->query('BEGIN');

    // Update the balance of the user with the amount specified.
    $stmt = $conn->prepare('UPDATE `table` SET `balance` = `balance` + ? WHERE `user` = ?');
    $stmt->bind_param ('dd', $amount, $userID);

    // If query fails, then roll back and return/throw an error condition.
    if (!$stmt->execute ()) {
        $conn->query ('ROLLBACK');
        throw new Exception ('Count not perform query!');
    }

    // We need the updated balance to check if the user has a positive credit counter now.
    $stmt = $conn->prepare ('SELECT `balance` FROM `table` WHERE `user` = ?');
    $stmt->bind_param ('d', $userID);

    // Same as last time.
    if (!$stmt->execute ()) {
        $conn->query ('ROLLBACK');
        throw new Exception ('Count not perform query!');
    }

    $stmt->bind_result($amount);
    $stmt->fetch();

    // We need to inform the user if he doesn't have enough credits.
    if ($amount < 0) {
        $conn->query ('ROLLBACK');
        return false;
    }

    // Everything is good at this point.
    $conn->query ('COMMIT');
    return true;
}

答案 4 :(得分:0)

也许你的问题只是存储余额的方法。你为什么把它放在一个领域?你失去了做这件事的所有历史。

创建一个表:transactions_history。然后对于每个事务,执行INSERT查询,传递用户,事务值和操作(存入或取消)。

然后,要向您的用户显示他当前的余额,只需对其所有交易历史记录执行SELECT,正确执行操作,最后他将看到实际的正确余额。而且你也可以防止错误同时进行2次UPDATE查询(虽然&#34;同一时间&#34;它并不像我们想象的那么常见)。

答案 5 :(得分:-2)

你可以使用这样的交易。 $ balance是你要减去的余额。如果查询执行得好,它将显示更新的余额,否则它将回滚到初始位置,异常错误将显示失败错误。

try {
            $db->beginTransaction();
            $db->query('update tableA set balance=balance-'".$balance."' where user='xxx'" ');
            $db->commit();
        } catch (Exception $e) {
            $db->rollback();
        }