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

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.

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.

Avoiding Double Booking / Double Spending

I'm running a high throughput application in which many auction-like processes happen every second and budgets are spent based on auction-winning bids.
However, there's a slight latency between the moment a bid is placed and the time when the auction is decided, so what started happening is this:
two similar auctions are won by the same highest bidder in the same second, but the total bids for the 2 auctions exceed the budget of the user, so by the time the second auction tries to get the bid, the first one already got it and the user now ends up with a negative balance.
I do understand this is a complicated problem, but I would like to know your opinions.
Some details about my application might offer insights into what the right tradeoffs might be:
the bids are, generally, much smaller than the user budget, so not allowing the users to spend the last few cents of their $5 budget might not be a big issue, but there's no limit to the bids, so this might not, in fact, help decreasing risk (might work for the low-bid transactions, but as soon as the user places, say, a $3 bid, the budget can still go from $5 to -$7 in a second, by winning just 4 auctions with this higher than normal bid).
not allowing bids higher than a certain budget ratio can also be an acceptable solution, but this might affect the user experience quite a lot and the bid/budget ratio will be rather arbitrary. The lower the ratio, the safer the budget and the worse user experience, but there's still no guarantee the budget won't be exceeded.
booking the bid for a certain time can also be an option, but with 100k users bidding multiple times per second, the solution for booking all the bids and releasing the funds can become quite complex
simply not paying for the second transaction can also be a (rather crappy and unfair) solution, but it might limit the incentive to abuse this bug.
I realize these details are all direct attempts to solve the problem, but none of them is good enough and I'm curious what your opinion is and if there are any better solution to the Double Spending problem. Unlike the solution found for the Bitcoin protocol, in my case the exchange of "goods" is done in real time and I cannot revert it once there are no more funds available (the goods, in my case, are website visits, so they're instantaneous, can't be stored until the transaction is settled and can't be delayed).
This might work: change the rule for winning an auction.
Now, I guess, a user wins an auction if she's the highest bidder at the conclusion of the auction. You can change the rule so the user must BOTH be the high bidder AND have sufficient budget to cover the bid. If both criteria aren't met, the next highest bidder with enough budget wins.
This is easy to explain to users. "to win, you need enough in your budget. If you're losing auctions you can increase your budget."
Implementation wise, you can use a database transaction to handle the "win" operation. Inside a single database transaction, debit the winning buyer's budget and credit the seller's account.
You can use SQL sequences like this:
START TRANSACTION;
SELECT budget
FROM user
WHERE id = nnnn
AND budget >= bid
FOR UPDATE;
/* if you got a result do this */
UPDATE user SET budget=budget-bid
WHERE id= nnnn;
UPDATE seller SET balance=balance+bid
WHERE ID = sssss;
UPDATE auction
SET winner=nnnn, winning_bid=bid,
closetime=NOW()
WHERE auction = aaaa;
COMMIT;
/* if you got no result from the SELECT do this */
ROLLBACK;
You can use distributed locks to solve the double-spending /double booking issue. You need to acquire at least three locks that would update
User's budget i.e. bidder's account balance
Seller's account balance
Auction
You can also create a bid ledger to verify the user's spending, seller account balance, and user's wallet balance. You can run a cron job hourly or every 10 minutes to verify any error and notify support/dev to check for potential errors.
Your logic could be like this
Procedure BidProcessor
input: user, bid, seller
// this should get locks for auction, user wallet balance and seller balance
success = acquireLocks( bid.auction_id, user.id, seller.id )
if success then
hasSufficientFund = user.hasSufficientFund( bid.amount )
if hasSufficientFund then
ExecuteBid( user, bid, seller )
releaseLocks( bid.auction_id, user.id, seller.id )
else
releaseLocks( bid.auction_id, user.id, seller.id )
endif
else
releaseLocks( bid.auction_id, user.id, seller.id )
endif
Each executor can use their identity id as a lock value to avoid releasing someone else lock.
Procedure ExecuteBid
input: user, bid, seller
Description: Run a SQL transaction to update all related entities
START TRANSACTION;
UPDATE user SET budget=budget-bidAmount WHERE id= user.id;
UPDATE seller SET balance=balance+bidAmount WHERE ID = seller.id;
UPDATE auction SET winner=bid.id, closed_at=NOW() WHERE id = bid.auction_id;
UPDATE bid SET processed_at = NOW(), status='WON' WHERE id = bid.id;
COMMIT;
if commit fails then do a rollback
ROLLBACK;

MySQL selling database consistency: queued transactions?

I'm trying to create an application that has the ability to sell gift cards but I'm fearing the consistency of data, so let me explain what I mean:
I have 3 tables
transactions(id, created_date),
cards(id, name) and
vouchers(id, code, card_id, transaction_id).
The database contains many cards and vouchers and each voucher belongs to one card.
The user will want to select a card to buy and choose a quantity.
So, the application will select vouchers according to the selected card and LIMIT according to quantity then creating a new transaction and adding the transaction_id into the selected vouchers to flag them as bought cards for this new transaction.
So, what I am afraid of is what if multiple users sent the same buying request for the same card at the exact same time, then will some data collision occur? and what is the best approach to fix this?
I am currently using MySQL 8 Community Edition with InnoDB engine for all tables.
What I am searching for is if I can create some kind of a queue that activates transactions one by one without collision even if it means that the users will have to wait until they get to their turn in the queue.
Thanks in advance
This is a job for MySQL transactions. With SQL you can do something like this.
START TRANSACTION;
SELECT id AS card_id FROM cards WHERE name = <<<<chosen name>>>> FOR UPDATE;
--do whatever it takes in SQL to complete the operation
COMMIT;
Multiple php processes, may try to do something with the same row of cards concurrently. Using START TRANSACTION combined with SELECT ... FOR UPDATE will prevent that by making the next process wait until the first process does COMMIT.
Internally, MySQL has a queue of waiting processes. As long as you do the COMMIT promptly after you do BEGIN TRANSACTION your users won't notice this. This most often isn't called "queuing" at the application level, but rather transaction consistency.
Laravel / eloquent makes this super easy. It does the START TRANSACTION and COMMIT for you, and offers lockForUpdate().
DB::transaction(function() {
DB::table('cards')->where('name', '=', $chosenName)->lockForUpdate()->get();
/* do whatever you need to do for your transaction */
});

MySql implementing increase and descrase fields and get result

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.

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