MySql implementing increase and descrase fields and get result - php

In my application i want to implementing this mySql command as a single command to use that and get result on time without use any other command by programing out of this box such as PHP or etc,
what i want to implementing action:
check user money
IF user has money then
decrease money from himself
AND
increase money of other user
RETURN result
ELSE
RETURN result as false
this command is my implementation but its not correct
SELECT *, (case when (money >= 200)
THEN
if(
(update money_repositories set money = money-200 where userId = 1)
AND
(update money_repositories set money = money+200 where userId = 34)
) as state
ELSE
false
END)
as state from money_repositories where userId = 1;
how can i fix this command? Thank you very much

What we have here is a financial transaction. It would be horrible if the money was deducted from the first user and not second user. Is it a coincidence then that mysql has something called a transaction?
You cannot have an update inside a select. You need to have two different update statements here. First to deduct from user1, second to credit into user2's account. Transactions ensure that both operations succeed together or the first query is rolled back preserving user1's money.
The other aspect of transactions ensure's that another thread does not make a similiar modification changing the balance between the two update queries.

Related

PHP Race condition for Raffles

I need to create a Raffle system where users can enter the number of tickets they want to buy and pay with credit cards to participate in a raffle. A raffle has a limited number of tickets let's say 1000. Anyone can enter any number of tickets he wants to buy, of course the number should be less or equal to 1000. There is no login in the system so I'm seeing this following as a race condition:
One user enters 998 tickets to buy and another one enters 5 tickets, if both users click on submit on the same time and I process both requests is this going to be a race condition? If yes, has anyone came across a similar case and is there any way to avoid this?
Thanks.
Many programmers will approach this by checking the database for sufficient tickets and if found then updating the ticket sales. This leaves a window between the read and write parts of the operation where the race condition exists.
User A reads (reads 1000 tickets)
User B reads (reads 1000 tickets)
User A writes(reduces tickets left by 998, writes 2)
User B writes (reduces tickets left by 5, writes 995)
We're left with 1003 tickets 'sold', and a remaining balance of 995 tickets to sell. This is clearly not acceptable.
What's required is an atomic test-and-set operation (atomic, because it's indivisible)
Fortunately, databases treat single queries as atomic, and also provide a mechanism for the test-and-set requirement
Consider the query
UPDATE `raffles` set `ticketsLeft` = `ticketsLeft`- 995 WHERE `raffleID` = 'someId' and `ticketsLeft` >= 995
This will test for sufficient tickets and deduct the tickets sold all as part of one query. There's no window in which a race can exist.
But, how does the program know that the update succeeded?
The database handles that too. The program asks the database for the number of rows affected by the UPDATE query. If the update succeeds one row is affected. If it fails (no raffleId or insufficient tickets) no rows are affected.
Thus, the program executes its query, and checks the rows affected. If the answer is 1 the ticket sale succeeded. If the answer is zero, the ticket sale failed. The program handles these two possibilities and carries on.*
For PHP there are two interfaces to MySQL: see mysqli::affected_rows(), or PDOStatement::rowcount() for the details.
Other databases have similar constructions.
* Data integrity is assured here by applying a UNIQUE index to the raffleId column, guaranteeing there will only be one matching raffle, or none.

Does SELECT....FOR UPDATE actually delay the read?

So here is my scenario, lets assume I am making an online shopping platform. And My User have a balance of 100 in the user_balance field or table.
Now, the user, open both the withdrawal page which let them withdraw money and a shopping page which let him to buy a watch of 100 dollar with one click
Let say the user withdraw 100 dollar and buy a watch for 100 dollar at the same time.
My question is will the SELECT user_balance FROM balances FOR UPDATE execute at the same time or it will wait other to finish select.
If both the SELECT...FOR UPDATE execute at the same time, the user_balance will show 100 for both page and thus, it will allow withdrawal of 100 and purchase a watch for 100 and hence, when we finally update the balance of the user it will show a negative balance
100(user balance) - 100(withdrawal amount) - 100(purchasing of watch) = -100
Here is concept of code of both pages:
Withdrawal Pages:
$withdrawal_amount = 100;
$user_balance = "SELECT user_balance FROM balances FOR UPDATE"; //actually return 100?(not sure about it, that is what my question about)
if($user_balance > $withdrawal_amount){
//allow withdrawal
$update_sql_query = "UPDATE balances SET user_balance = user_balance - " . $withdrawal_amount;
}
Purchase Watch Page:
$product_subtotal = 100;
$user_balance = "SELECT user_balance FROM balances FOR UPDATE"; //actually return 100?(not sure about it, that is what my question about)
if($user_balance > $product_subtotal){
//allow withdrawal
$update_sql_query = "UPDATE balances SET user_balance = user_balance - " . $product_subtotal;
}
The correct approach here would seem to be for each operation to run in a separate transaction using SELECT ... FOR UPDATE. In pseudo code, the process for the withdrawal (or the purchase) would look something like this:
start transaction
SELECT user_balance FROM balances FOR UPDATE;
UPDATE balances SET user_balance = user_balance - 100;
end transaction
This pattern works here as follows. The transaction obtains an exclusive lock on the user balance record being updated. This means that any other transaction which tries to read the user balance before it has been debited will block, and will have to wait. This avoids the situation of two transactions interleaving resulting in an incorrect balance.
Note that locking reads require the InnoDB engine. Check the MySQL documentation for more information.
Does SELECT FOR UPDATE delay the read?
Yes. You need to use transactions to get good results.
SELECT ... FOR UPDATE, when done inside a MySQL transaction in a InnoDB table, locks the row or rows selected. Let's assume your code actually selects just one row, by doing SELECT something FROM balances WHERE id=something FOR UPDATE.
Then if two different programs connected to MySQL try to do that SELECT on the same row at roughly the same time, one of them will win. That is, it will get there first, and the query will complete.
To get this to work properly, wrap all the work you need to do in START TRANSACTION and COMMIT. The first thing you should do after the START TRANSACTION should be your SELECT ... FOR UPDATE.
If, while you're doing your work you decide the user cannot do what she wants to do, you can issue ROLLBACK in place of COMMIT and all the changes in the transaction will be abandoned.
The second program's query will not complete until the first program does COMMIT to complete its transaction. Then it will read the whatever was stored into the table in during that transaction.
These are the things to keep in mind: SQL transactions look, to other programs connected to the table server, like they happened all at once. When one program has a transaction in progress, other programs wait. Most of the time transactions complete quickly so it's hard to observe the wait time.

PHP/MySQL critical error [duplicate]

I have a game website and I want to update the users money, however if I use 2 pc's at the exact same time this code will execute twice and the user will be left with minus money. How can I stop this from happening? It's driving me crazy.
$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();
Any help is appreciated.
Echoing the comment from #GarryWelding: the database update isn't an appropriate place in the code to handle the use case that is described. Locking a row in the user table isn't the right fix.
Back up a step. It sounds like we are wanting some fine grained control over user purchases. Seems like we need a place to store a record of user purchases, and then we can can check that.
Without diving into a database design, I'm going to throw out some ideas here...
In addition to the "user" entity
user
username
account_balance
Seems like we are interested in some information about purchases a user has made. I'm throwing out some ideas about the information/attributes that might be of interest to us, not making any claim that these are all needed for your use case:
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 (e.g. purchase is rejected, "insufficient funds", "duplicate item"
We don't want to try to track all of that information in the "account balance" of a user, especially since there can be multiple purchases from a user.
If our use case is much simpler than that, and we only to keep track of the most recent purchase by a user, then we could record that in the user entity.
user
username
account_balance ("money")
most_recent_purchase
_datetime
_item_service
_amount ("money")
_from_computer/session
And then with each purchase, we could record the new account_balance, and overwrite the previous "most recent purchase" information
If all we care about is preventing multiple purchases "at the same time", we need to define that... does that mean within the same exact microsecond? within 10 milliseconds?
Do we only want to prevent "duplicate" purchases from different computers/sessions? What about two duplicate requests on the same session?
This is not how I would solve the problem. But to answer the question you asked, if we go with a simple use case - "prevent two purchases within a millisecond of each other", and we want to do this in an UPDATE of user table
Given a table definition like this:
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)
with the datetime (down to the microsecond) of the most recent purchase recorded in the user table (using the time returned by the database)
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
)
We can then detect the number of rows affected by the statement.
If we get zero rows affected, then either :user wasn't found, or :money2 was greater than the account balance, or most_recent_purchase_dt was within a range of +/- 1 millisecond of now. We can't tell which.
If more than zero rows are affected, then we know that an update occurred.
EDIT
To emphasize some key points which might have been overlooked...
The example SQL is expecting support for fractional seconds, which requires MySQL 5.7 or later. In 5.6 and earlier, DATETIME resolution was only down to the second. (Note column definition in the example table and SQL specifies resolution down to microsecond... DATETIME(6) and NOW(6).
The example SQL statement is expecting username to be the PRIMARY KEY or a UNIQUE key in the user table. This is noted (but not highlighted) in the example table definition.
The example SQL statement overrides update of user for two statements executed within one millisecond of each other. For testing, change that millisecond resolution to a longer interval. for example, change it to one minute.
That is, change the two occurrences of 1000 MICROSECOND to 60 SECOND.
A few other notes: use bindValue in place of bindParam (since we're providing values to the statement, not returning values from the statement.
Also make sure PDO is set to throw an exception when an error occurs (if we aren't going to check the return from the PDO functions in the code) so the code isn't putting it's (figurative) pinky finger to the corner of our mouth Dr.Evil style "I just assume it will all go to plan. What?")
# 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
}
And to emphasize a point I made earlier, "lock the row" is not the right approach to solving the problem. And doing the check the way I demonstrated in the example, doesn't tell us the reason the purchase was unsuccessful (insufficient funds or within specified timeframe of preceding purchase.)
for the negative balance change your code to
$sql = "UPDATE users SET money = money- :money WHERE username=:user AND money >= :money";
First idea:
If you're using InnoDB, you can use transactions to provide fine-grained mutual exclusion. Example:
START TRANSACTION;
UPDATE users SET money = money- :money WHERE username=:user;
COMMIT;
If you're using MyISAM, you can use LOCK TABLE to prevent B from accessing the table until A finishes making its changes. Example:
LOCK TABLE t WRITE;
UPDATE users SET money = money- :money WHERE username=:user;
Second idea:
If update don't work, you may delete and insert new row (if you have auto increment id, there won't be duplicates).

MySQL using COUNT as total and avoiding exceeding a maximum

I was tasked to create this organization registration system. I decided to use MySQL and PHP to do it. Each organization in table orgs has a max_members column and has a unique id org_id. Each student in table students has an org column. Every time a student joins an organization, his org column is equated to the org_id of that organization.
When someone clicks join on an organization page, a PHP file executes.
In the PHP file, a query retrieves the total number of students whose org is equal to the org_id of the organization being joined.
$query = "SELECT COUNT(student_id) FROM students WHERE org = '$org_id'";
The maximum members is also retrieved from the orgs table.
$query = "SELECT max_members FROM orgs WHERE org_id = '$org_id'";
So I have variables $total_members and $max_members. A basic if statement checks if $total_members < $max_members, then updates the student's org equal to the org_id. If not, then it does nothing and notifies the student that the organization is full.
What my main concern is what if this situation happened:
Org A only has one slot left. 29/30 members.
Student A clicks join on Org A (and at the same time)
Student B clicks join on Org A
Student A retrieves data: There is one slot left
Student B retrieves data: There is one slot left
Student A's org = Org A's org_id
Student B's org = Org A's org_id
After the scripts have executed, Org A will show up with 31/30 members
Can this happen? If yes, how can I avoid it?
I've thought about using MySQL variables like this:
SET #org_id = 'what-ever-org';
SELECT #total_members := COUNT(student_id) FROM students WHERE org_main = #org_id;
SELECT #max_members := max_members FROM orgs WHERE org_id = #org_id;
UPDATE students SET org_main = IF(#total_members < #max_members, #org_id, '') WHERE student_id = 99999;
But I don't know if it would make a difference.
Row locking does not apply in my case. I think. I'd love to be proven wrong though.
The code I've written above is a simplified version of the original code. The original code included checking registration dates, org days, etc, however, these things are not related to the question.
What you're describing is usually called a race-condition. It occurs, because you perform two non-atomic operations on your database. To avoid this you need to use transactions, which ensure that the database server prevents this kind of interference. Another approach would be to use a "before update trigger".
Transaction
As you're using MySQL you have to make sure that the DB engine your tables are running on is InnoDB, because MyISAM just doesn't have transactions. Before you do your SELECT you need to start a transaction. Either send START TRANSACTION manually to the database or use a proper PHP implementation for it, e.g. PDO (PDO::beginTransaction()).
In a running transaction you can then use the suffix FOR UPDATE in your SELECT statement, which will lock the rows that have been selected by the query:
SELECT COUNT(student_id) FROM students WHERE org = :orgId FOR UPDATE
After your UPDATE statement you can commit the transaction, which will write the changes permanently to the database and remove the locks.
If you expect a lot of these simultaneous requests to happen, be aware that locking can cause some delay in the response, because the database might wait for a lock to be released.
Trigger
Instead of using transactions you can also create a trigger on the database, which will run before an update is executed on the database. In this trigger you could test if the maximum number of students has been exceeded and throw an error if that's the case. However, this can be a very challenging approach, especially if the value to be checked depends on something in the UPDATE statement. Also it is debatable if it's a good idea to implement this kind of logic on the database level.
There are two ways, use synchronized function in PHP which performs this operation. But if you want to implement all the logic in MySQL (I'd like this method), please use Stored Procedure.
Create a stored procedure as (not exactly):
CREATE PROCEDURE join_org(stu_id int, org_id, int, OUT success int)
DECLARE total_members int;
DECLARE max_members_Allowed;
SELECT COUNT(student_id) INTO total_members FROM students WHERE org = 'org_id';
SELECT max_members INTO max_members_Allowed FROM orgs WHERE org_id = 'org_id';
IF(max_members_Allowed > total_members) Then
UPDATE student SET orgid='org_id';
SET success = 1;
ELSE
SET success = 0;
END IF;
Then register this out variable named 'success' int your PHP code to indicate success or failure. Call this procedure when user clicks join.

Multiple update to the same row in database

I'm making a Billing system with some friends, it works this way:
The customers make calls.
The customers hangup the call.
The price of the call is calculated.
The price of the call is reduced from the customer's credit.
We decided to make the following:
Get the user's balance and store it in a variable, $balance, after do a $balance = $balance - $callprice, and finally update the database.
The problem is that the customer can make simultaneous calls, and if two calls finish at the same time, and one of them gets the value on the database before the other script had updated the new balance... one of the calls will be lost. I'm using php.
Any idea how can I do it?
Thanks, and sorry, I have a poor English...
The problem is it looks like you're trying to use two SQL statements to update the user's balance: One to SELECT the user's balance, then another to UPDATE the user's balance after the balance is subtracted using PHP.
You could do it all in one operation and eliminate the possibility of race-conditions:
UPDATE users
SET balance = balance - <callprice here>
WHERE user_id = <user_id here>

Categories