- Begin transaction
- Select items in shopping cart (with for update lock)
So far so good, this will at least prevent the user from doing checkout in multiple sessions (multiple times trying to checkout the same card - good to deal with double clicks.)
- Check if items haven't been booked by other user
How do you check? With a standard SELECT
or with a SELECT ... FOR UPDATE
? Based on step 5, I'm guessing you are checking a reserved column on the item, or something similar.
The problem here is that the SELECT ... FOR UPDATE
in step 2 is NOT going to apply the FOR UPDATE
lock to everything else. It is only applying to what is SELECT
ed: the cart-item
table. Based on the name, that is going to be a different record for each cart/user. This means that other transactions will NOT be blocked from proceeding.
- Make payment
- Update items marking them as reserved
- If everything went fine commit transaction, rollback otherwise
Following the above, based on the information you've provided, you may end up with multiple people buying the same item, if you aren't using SELECT ... FOR UPDATE
on step 3.
Suggested Solution
- Begin transaction
SELECT ... FOR UPDATE
the cart-item
table.
This will lock a double click out from running. What you select here should be the some kind of "cart ordered" column. If you do this, a second transaction will pause here and wait for the first to finish, and then read the result what the first saved to the database.
Make sure to end the checkout process here if the cart-item
table says it has already been ordered.
SELECT ... FOR UPDATE
the table where you record if an item has been reserved.
This will lock OTHER carts/users from being able to read those items.
Based on the result, if the items are not reserved, continue:
UPDATE ...
the table in step 3, marking the item as reserved. Do any other INSERT
s and UPDATE
s you need, as well.
Make payment. Issue a rollback if the payment service says the payment didn't work.
Record payment, if success.
Commit transaction
Make sure you don't do anything that might fail between steps 5 and 7 (like sending emails), else you may end up with them making a payment without it being recorded, in the event the transaction gets rolled back.
Step 3 is the important step with regards to making sure two (or more) people don't try to order the same item. If two people do try, the 2nd person will end up having their webpage "hang" while it processes the first. Then when the first finishes, the 2nd will read the "reserved" column, and you can return a message to the user that someone has already purchased that item.
Payment in transaction or not
This is subjective. Generally, you want to close transactions as quickly as possible, to avoid multiple people being locked out from interacting with the database at once.
However, in this case, you actually do want them to wait. It's just a matter of how long.
If you choose to commit the transaction before payment, you'll need to record your progress in some intermediate table, run the payment, and then record the result. Be aware that if the payment fails, you'll then have to manually undo the item reservation records that you updated.
SELECT ... FOR UPDATE on non-existent rows
Just a word of warning, in case your table design involves inserting rows where you need to earlier SELECT ... FOR UPDATE
: If a row doesn't exist, that transaction will NOT cause other transactions to wait, if they also SELECT ... FOR UPDATE
the same non-existent row.
So, make sure to always serialize your requests by doing a SELECT ... FOR UPDATE
on a row that you know exists first. Then you can SELECT ... FOR UPDATE
on the row that may or may not exist yet. (Don't try to do just a SELECT
on the row that may or may not exist, as you'll be reading the state of the row at the time the transaction started, not at the moment you run the SELECT
. So, SELECT ... FOR UPDATE
on non-existent rows is still something you need to do in order to get the most up to date information, just be aware it will not cause other transactions to wait.)