Concurrency scenarios with INSERTs

Nitseg

I'm designing a booking system in PHP + PostgreSQL. I'm not able to find a clean solution to a concurrency problem based on INSERTs operations.

The DB system is mainly made of these tables:

CREATE TABLE booking (
booking_id INT,
user_id INT,
state SMALLINT,
nb_coupons INT
);

CREATE booking_state_history (
booking_state_history_id INT,
timestamp TIMESTAMP,
booking_id INT,
state SMALLINT);

CREATE TABLE coupon_purchase(
coupon_purchase_id,
user_id INT,
nb INT,
value MONEY)

CREATE TABLE coupon_refund(
coupon_refund_id INT,
user_id,
nb INT,
value MONEY)

CREATE TABLE booking_payment(
booking_payment_id INT,
user_id,
booking_id,
nb INT,
value MONEY)

A booking must be paid with coupons that have been previously purchased by the user. Some coupons may have been refund. All these operations are stored in the two corresponding tables to keep an history and be able to compute the coupon balance. Constraint: the coupon balance cannot be negative at any time.

A booking is finalized when it is paid with coupons.

Then the following operations happen:

BEGIN;    
(1) Check there are enough coupons remaining to pay the booking. (SELECT)    
(2) Decide which coupons (number and value) will be used to pay the booking 
(mainly, higher cost coupon used first. But that is not the issue here.)    
(3) Add records to booking_payment (INSERTs)    
(4) Move the booking to state="PAID" (integer value representing "PAID") (UPDATE)
(5) Add a record to booking_state_history (INSERT)    
COMMIT;

These operations need to be atomic to preserve DB information coherency.

Hence the usage of transactions that allow to COMMIT or ROLLBACK in case of failure, DB exception, PHP exception or any other issue in the middle of the operations.

Scenario 1

Since I'm in a concurrent access environment (web site) nothing prevents the user from (for instance) asking for a coupon refund while doing a booking payment at the same time.

Scenario 2

He can also trigger two concurrent booking payments at the same time in two different transactions.

So the following can happen:

Scenario 1 After (1) is done, the coupon refund is triggered by the user and the subsequent coupon balance is not enough to pay the booking any more. When it COMMITs the balance becomes negative. Note: Even if I do a recheck of coupon balance in a new (6) step, there is a possibility for the coupon refund to happen in the meantime between (6) and COMMIT.

Scenario 2

Two concurrent booking payment transactions for which total number of coupons for payment is too much for the global balance to stay positive. Only one of them can happen. Transaction 1 and transaction 2 are checking for balance and seeing enough coupons for their respective payment in step (1). They go on with their operations and COMMIT. The new balance is negative and conflicting with the constraint. Note: Even if I do a coupon balance recheck in a new (6) step the transactions cannot see the operations not yet commited by the other one. So they blindly proceed to COMMIT.

I guess this is an usual concurrency case but I cannot find a pattern to solve this on the internet.

I thought of rechecking the balance after the COMMIT so I can manually UNDO all the operations. But it is not totally safe since if an exception happen after the commit the UNDO won't be done.

Any idea to solve this concurrency problem?

Thanks.

Pavel Horal

Your problem boils down to the question of "what should be the synchronization lock". From your question it seems that the booking is not booking of a specific item. But lets assume, that a user is booking a specific hotel room so you need to solve two problems:

  • prevent overbooking (e.g. booking the same thing for two people)
  • prevent parallel account state miscalculation

So when a user gets to a point when he/she is about to hit confirm button, this is a possible scenario you can implement:

  1. begin transaction

  2. lock the user entry so that parallel processes are blocked

    SELECT * FROM user FOR UPDATE WHERE id = :id

  3. re-check account balance and throw exception / rollback if there are insufficient funds

  4. lock the item to be booked to prevent overbooking

    SELECT * FROM room FOR UPDATE WHERE id = :id

  5. re-check booking availability and throw exception / rollback if the item is already booked

  6. create booking entry and subtract funds from user's account

  7. commit transaction (all locks will be released)

If, in your case, you don't need to check for overbooking, just skip / ignore steps 4 and 5.

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related