使用INSERT

时间:2015-06-24 09:40:17

标签: postgresql concurrency transactions

我正在设计PHP + PostgreSQL的预订系统。 我无法找到基于INSERT操作的并发问题的干净解决方案。

数据库系统主要由这些表组成:

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)

必须使用之前由用户购买的优惠券支付预订。有些优惠券可能已退款。所有这些操作都存储在两个相应的表中以保存历史记录并能够计算优惠券余额。 约束:优惠券余额在任何时候都不能为负数。

使用优惠券支付预订时最终确定。

然后发生以下操作:

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;

这些操作需要是原子的,以保持DB信息的一致性。

因此,在发生故障,数据库异常,PHP异常或操作过程中的任何其他问题时,允许使用 COMMIT ROLLBACK 的事务。< / p>

情景1

由于我处于并发访问环境(网站),所以没有什么能阻止用户(例如)在同时进行预订付款时要求优惠券退款。

场景2

他还可以在两个不同的交易中同时触发两笔并发预订付款。

所以可能发生以下情况:

场景1 在(1)完成之后,优惠券退款由用户触发,并且随后的优惠券余额不足以支付预订。 当 COMMITs 时,余额变为负数。 注意: 即使我在新的(6)步骤中重新检查优惠券余额,优惠券退款也可能在此期间(6)和 COMMIT 之间发生。

场景2

两笔并发预订支付交易,其中支付的优惠券总数太多,无法保持全球余额。只有其中一个可以发生。 交易1和交易2正在检查余额并在步骤(1)中查看足够的优惠券以用于其各自的支付。 他们继续他们的运营和 COMMIT 。新的平衡是负面的,与约束相冲突。 注意: 即使我在新的(6)步骤中重新检查优惠券余额,交易也看不到另一个尚未提交的操作。 所以他们盲目地进入 COMMIT

我想这是一个通常的并发案例,但我无法在互联网上找到解决这个问题的模式。

我想在COMMIT之后重新检查余额,所以我可以手动 UNDO 所有操作。但这并不完全安全,因为如果在提交后发生异常, UNDO 将无法完成。

有什么想法解决这个并发问题?

感谢。

2 个答案:

答案 0 :(得分:1)

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.

答案 1 :(得分:0)

以下是我已实施的解决方案。

注意:我刚刚处理了以下优惠券转帐部分,但预订状态更改和booking_state_history相同。

主要思想是将这部分处理保留为关键部分。 当要进行booking_payment,coupon_purchase或coupon_refund的INSERT时,我通过对给定user_id的UPDATE锁定专用表来阻止其他事务执行相同操作。

这样,只会锁定影响此给定user_id的同一种处理的事务。

<强> Intialization

DROP TABLE coupon_purchase;
DROP TABLE coupon_refund;
DROP TABLE booking_payment;
DROP TABLE lock_coupon_transaction;

CREATE TABLE coupon_purchase(
coupon_purchase_id SERIAL PRIMARY KEY,
user_id INT,
nb INT);

CREATE TABLE coupon_refund(
coupon_refund_id SERIAL PRIMARY KEY,
user_id INT,
nb INT);

CREATE TABLE booking_payment(
booking_payment_id SERIAL PRIMARY KEY,
user_id INT,
booking_id INT,
nb INT);

CREATE TABLE lock_coupon_transaction (
user_id INT,
timestamp TIMESTAMP); 

INSERT INTO coupon_purchase
(user_id, nb) VALUES
(1, 1),
(1, 5);

INSERT INTO coupon_refund
(user_id, nb) VALUES
(1, 3);

INSERT INTO lock_coupon_transaction
(user_id, timestamp) VALUES
(1, current_timestamp);

交易1

BEGIN;

UPDATE lock_coupon_transaction SET timestamp=current_timestamp WHERE user_id='1';

WITH coupon_balance AS (
SELECT
  t1.nb_purchased_coupons -
  t2.nb_refunded_coupons -
  t3.nb_booking_payment_coupons AS total
FROM 
(SELECT COALESCE(SUM(nb),0) AS nb_purchased_coupons FROM coupon_purchase WHERE user_id='1' ) t1,
(SELECT COALESCE(SUM(nb),0) AS nb_refunded_coupons FROM coupon_refund WHERE user_id='1' ) t2,
(SELECT COALESCE(SUM(nb),0) AS nb_booking_payment_coupons FROM booking_payment WHERE user_id='1' ) t3
)
INSERT INTO booking_payment
(user_id, booking_id, nb)
SELECT 1::INT, 1::INT, 3::INT
FROM coupon_balance
WHERE (total::INT >= 3::INT);
INSERT 0 1

交易2

BEGIN;

UPDATE lock_coupon_transaction SET timestamp=current_timestamp WHERE user_id='1';

// Transaction is locked waiting for a COMMIT or ROLLBACK from transaction 1.

交易1

COMMIT;
COMMIT

交易2

// Transaction 1 lock has been released so transaction 2 can go on

WITH coupon_balance AS (
SELECT
  t1.nb_purchased_coupons -
  t2.nb_refunded_coupons -
  t3.nb_booking_payment_coupons AS total
FROM 
(SELECT COALESCE(SUM(nb),0) AS nb_purchased_coupons FROM coupon_purchase WHERE user_id='1' ) t1,
(SELECT COALESCE(SUM(nb),0) AS nb_refunded_coupons FROM coupon_refund WHERE user_id='1' ) t2,
(SELECT COALESCE(SUM(nb),0) AS nb_booking_payment_coupons FROM booking_payment WHERE user_id='1' ) t3
)
INSERT INTO coupon_refund
(user_id, nb)
SELECT 1::INT, 3::INT
FROM coupon_balance
WHERE (total::INT >= 3::INT);
INSERT 0 0 

COMMIT;
COMMIT

由于账户资金不足,无法完成INSERT。这是预期行为

上一个交易是在第二个交易进行时提交的。因此,事务2可以看到事务1所做的所有更改。

这样就不存在同时访问优惠券处理的风险。

相关问题