简体   繁体   中英

SQL prevent select same field at same time

i want to get last balance and update some transaction of xxx user from backend.. unfortunately, at the same time, xxx also do the transaction from frontend, so when I processed my query, xxx is processing same query too, so it get same last balance.

here is my script.
assume : xxx last balance is 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'");

at the same time user xxx do transaction from frontend..

$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'");

how can I done my query first, then user xxx may processed the query?

You can lock the table " TableA " using the MySQL LOCK TABLES command.

Here's the logic flow :

  • LOCK TABLES "TableA" WRITE;

  • Execute your first query

Then

  • UNLOCK TABLES;

See:

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

Using InoBD engine and transaction to make it ACID( https://en.wikipedia.org/wiki/ACID )

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

In additional, why dont you use query to increate balance

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

This is one of the available approaches.

You have to use InnoDB engine for your table. InnoDB supports row locks so you won't need to lock the whole table for UPDATEing just one ROW related to a given user. (Table lock will prevent other INSERT/UPDATE/DELETE operations from being executed resulting in that they will have to wait for this table LOCK to be released).

In InnoDB you can achieve ROW LOCK when you are executing SELECT query by using FOR UPDATE . (but in this you have to use transaction to achieve the LOCK ). When you do SELECT ... FOR UPDATE in a transaction mysql locks the given row you are selecting until the transaction is committed. And lets say you make SELECT ... FOR UPDATE query in your backend for user entry XXX and at the same time frontend makes the same query for the same XXX. The first query (from backend) that was executed will lock the entry in the DB and the second query will wait for the first one to complete, which may result in some delay for the frontend request to complete.

But for this scenario to work you have to put both frontend and backend queries in transaction and both SELECT queries must have FOR UPDATE in the end.

So your code will look like this:

$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);

If this is your backend code, the frontend code should look very similar - having begin/commit transaction + FOR UPDATE .

One of the best thing about FOR UPDATE is that if you need a query to LOCK some row and do some calculations with this data in a given scenario but at the same time you need other queries that are selecting the same row and they do NO need the most recent data in that row, than you can simply do this queries with no transaction and with no FOR UPDATE in the end. So you will have LOCKED row and other normal SELECTs that are reading from it (of course they will read the old info ... stored before the LOCK started).

There are basically two ways you can go about this:

  • By locking the table.
  • By using transactions.

The most common one in this situation is using transactions, to make sure all of the operations you do are atomic. Meaning that if one step fails, everything gets rolled back to before the changes started.

Normally one would also do the operation itself in the query, for something as simple as this. As database engines are more than capable of doing simple calculations. In this situation you might want to check that the user actually has enough credit on his account, which in turn states that you need to check.
I'd just move the check to after you've subtracted the amount, just to be on the safe side. (Protection against racing conditions etc)

A quick example to get you started with:

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

Maybe your problem is just your way to store balance. Why do you put it in a field? You lose all the history of the transactions doing that.

Create a table: transactions_history. then for each transaction, do an INSERT query, passing the user, transaction value and operation (deposit or withdraw).

Then, to show to your user his current balance, just do a SELECT on all his transaction history, doing the operations correctly, in the end he will see the actual correct balance. And you also prevent the error from doing 2 UPDATE queries at the same (although "same time" its not so common as we may think).

you can use transaction like this. $balance is balance you want to subtract.if query perform well than it will show updated balance otherwise it will be rollback to initial position and exception error will show you the error of failure.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM