How to properly use transactions and locks to ensure database integrity?

后端 未结 2 561
广开言路
广开言路 2020-12-15 01:16

I develop an online reservation system. To simplify let\'s say that users can book multiple items and each item can be booked only once. Items are first added to the shoppin

2条回答
  •  [愿得一人]
    2020-12-15 02:07

    1. Begin transaction
    2. 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.)

    1. 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 SELECTed: 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.

    1. Make payment
    2. Update items marking them as reserved
    3. 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

    1. Begin transaction
    2. 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.

    1. 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:

    1. UPDATE ... the table in step 3, marking the item as reserved. Do any other INSERTs and UPDATEs you need, as well.

    2. Make payment. Issue a rollback if the payment service says the payment didn't work.

    3. Record payment, if success.

    4. 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.)

提交回复
热议问题