锁定表中的行以进行SELECT和UPDATE

时间:2011-02-24 03:28:44

标签: sql database-design postgresql

我正在编写一个需要预订电影院座位的剧本。

  1. 用户要求2个席位
  2. 如果有2个座位可用,系统会将其提供给客户
  3. 客户可以接受或请求另外2个席位。
  4. 当他最终接受时,座位被标记为“已售出”
  5. 由于可能有多个用户同时使用系统,我需要一种方法来“锁定”提供给当前客户端的行,直到某个时间过去,或者他请求另一个席位。

    目前,我将提供的座位标记为“已锁定”客户端ID,并使用SELECT将其返回给客户端(这是针对MySQL,但目标数据库是Postgres)

    UPDATE seats SET status = "locked", lock_time = NOW(), lock_id = "lock1" LIMIT 2
    SELECT * FROM seats WHERE lock_id = "lock1" AND lock_time > DATE_SUB(NOW(), INTERVAL 2 MINUTE)
    

    有一个问题:如果只有一个座位可用,它仍会被标记为“已锁定”,我将立即释放锁定。

    我也很确定有更聪明的方法。处理这样的任务的正确方法是什么?

6 个答案:

答案 0 :(得分:3)

您正在讨论的是预订系统。我建立这样的系统的方式是有一张预订表和座位表。

Create Table Reservations
    (
    EventId ... not null References Events ( Id )
    , SeatNumber varchar(10) not null
    , Expiration datetime not null
    , CustomerId ... not null References Customers( Id )
    , Constraint FK_Reservations_Seats
        Foreign Key( EventId, SeatNumber )
        References EventSeats( EventId, SeatNumber )
    )

Create Table EventSeats
    (
    EventId ... References Events ( Id )
    , SeatNumber varchar(10) not null
    , CustomerId ... null References Customers( Id )
    , PurchaseDate datetime not null
    )

当有人进行预订时,您会在预订表中插入一个日期时间值,以及将来指定的某段时间。当您查找可用座位时,您的查询如下:

Select S.EventId, S.SeatNumber
From EventSeats As S
Where S.EventId = ...
    And S.CustomerId Is Null
    And Not Exists  (
                    Select 1
                    From Reservations As R
                    Where R.EventId = S.EventId
                        And R.SeatNumber = S.SeatNumber
                        And R.Expiration > CURRENT_TIMESTAMP
                    )

如果他们愿意,这可以让某人暂时搁置在座位上。如果他们想要购买座位,您可以在将来的一段时间内插入另一个预订记录。事实上,我设计的系统在购买过程的每个步骤中插入了一个新的预订,这是在未来10分钟,以帮助用户在预订到期之前完成购买过程。完成购买后,您可以使用他们的信息更新EventSeats表,现在这个座位将永久保留。

答案 1 :(得分:2)

您可以使用SELECT ... FOR UPDATE为您锁定这些行 - 然后您可以计算出您选择了多少行,如果有足够的行,您可以使用锁定值和时间戳更新它们。如果您不再需要这些行,则可以ROLLBACK释放锁定。 http://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE

但是这些仅在事务持续时间内有效,如果事务丢失,这些锁将被释放,因此您无法使用SELECT ... FOR UPDATE锁来保持行打开,您需要将它们标记为保留某种方式。

通常,锁定然后等待用户响应是一种糟糕的技术。如果用户离开洗澡等等,那么你会留下许多锁定的行。

看来你有两个选择:

  1. 不要锁定任何东西,如果用户试图选择一个座位并且稍后售罄,只需道歉并将其与其他可用座位一起展示。此外,请记录这种情况发生的频率,如果您发现它经常发生,您可以考虑使用锁定方案。

  2. 执行您在问题中描述的内容,并制定一些规则,让座位预订在2分钟后过期等等......这样您就不必担心明确释放锁定,您只需检查时间戳即可当它们被设定时。

答案 2 :(得分:0)

如果我已正确理解您的问题,我认为解决方案可能如下:

进行以下交易(当然是伪代码)

<lock seats table>

  result=SELECT count(*) FROM seats 
   WHERE status="unlocked"
   GROUP BY status
   HAVING count(*)>=2

 IF result EXISTS

    UPDATE seats SET status = "locked", lock_time = NOW(), lock_id = "lock1" 
    WHERE status="unlocked"LIMIT 2


 <unlock seats table>

以这种方式快速释放表锁。然后,如果用户不想要他们的订单 可以通过简单的更新取消。锁定索引也不会阻止其他更新

另一种更合理的方法IMHO是以下伪代码。我认为这是一个很好的方法,因为它是无状态的,你不会保持记录被锁定(使用DB锁)等待用户决定(应该不惜一切代价避免)

 result=SELECT * FROM seats 
         WHERE status="unlocked"
 <present the result to the user and let the user decide which n seats they want>
 array[] choices:=<get from the user>

 //note that we do not lock the table here and the available seats presented to the     
 //user might be taken while he is making his choices. But that's OK since we should
 //not keep the table locked while he is making his choices. 

 <lock seats table>
 //now since the user either wants all the seats together or none of them, all the
 //seats rows that they want should be unlocked at first. If any of them
 // is locked when the UPDATE command is updating the row, then we should rollback all 
 // the updates. Unfortunately there is no way to determine that by standard update      
 // command. Thus here I use the following select query before making the update to
 // make sure every choice is there.

 result= SELECT count(*)
         FROM seats
         WHERE status="unlocked" AND seat_id IN (choice[1],choice[2], ...,choice[n])

 IF result=length(choices)

    UPDATE seats SET status = "locked", lock_time = NOW(), lock_id = "lock1" 
     WHERE seat_id IN (choice[1],choice[2], ...,choice[n])

  <unlock seats table>

此致 阿米尔

答案 3 :(得分:0)

只是一个替代方案 - 如果您只是'让'下一个客户也购买座位并向第一个用户提供错误弹出,以防他在第一个客户购买后选择这些座位,这意味着什么?

整个用例可以稍微改变一下(我正在考虑预订机票) -

客户选择电影 - 节目时间 - 显示所有空座位列表,并实时更新 - 使用颜色编码。然后,根据客户选择的座位,这些座位将被用于支付。

您使用的锁定机制将始终显示出比实际售罄更多的座位 - 这可能会不必要地导致销售损失。另一方面,如果您只是在用户实际购买座位时进行简单检查,在检索这些座位和预订座位之间检查座位是否已经出售给其他人,那么您始终可以显示错误消息。即便如此,在顾客选择座位直到付款之后,有必要锁定它们;但是你不会面临系统选择座位的问题,而是选择座位的顾客!

答案 4 :(得分:0)

在事务中执行update / select语句。如果只返回一行,则回滚事务并恢复锁定。

答案 5 :(得分:0)

竞争条件 - 我认为这会更好地作为插入而不是更新。两个更新可以同时运行,它们不会相互冲突。如果你有'锁定座位'表,那么你可以引用seat_id并使其独一无二。这样竞争条件就会失败。但是,在任何情况下,我都会将此作为更新写入问题,尽管您可以将其更改为插入内容。

如果没有足够的座位,您似乎不希望能够首先锁定座位。使用自联接这很容易:

create temp table seats
(
    id          serial,
    event_id    integer,
    locked      boolean default false
);
insert into seats (event_id) values (1),(1),(1),(2);
-- this will not lock event_id = 2 since it will not have a high enough count
update seats
set locked = true
from
(
    -- get the counts so we can drop events without enough seats
    select count(*), event_id from seats group by event_id
) as sum,
(
    -- you can not put limits in update; need to self-join
    select id from seats limit 2
) as t
where sum.event_id = seats.event_id
and seats.id = t.id
and count >= 2

UPDATE 2
 id | event_id | locked 
----+----------+--------
  3 |        1 | f
  4 |        2 | f
  2 |        1 | t
  1 |        1 | t
(4 rows)

因此,对于每个至少有两个座位的活动,这个'锁定'两个席位:)