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

发布于 2024-10-19 02:54:16 字数 613 浏览 1 评论 0 原文

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

  1. 用户请求 2 个席位
  2. 如果有 2 个席位可用,系统会将其提供给客户端
  3. 客户端可以接受它们或请求另外 2 个席位。
  4. 当他最终接受时,席位被标记为“已售出”。

由于可以有多个用户同时使用系统,我需要一种方法来“锁定”提供给当前客户的行,直到特定时间过去,或者他请求另一个席位。

目前,我使用客户端 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)

这样做有一个问题:如果只有 1 个席位可用,它仍然会被标记为“锁定”,我必须立即释放锁定。

我也非常确定有一种更聪明的方法可以做到这一点。处理这样的任务的正确方法是什么?

I am writing a script that needs to book seats in the cinema.

  1. User asks for 2 seats
  2. If there are 2 seats available, system offers them to client
  3. Client can either accept them or request another 2 seats.
  4. When he finally accepts, seats are marked as "sold"

Since there can be multiple users using system simultaneously, I need a way to "lock" the rows offered to current client until certain time passes, or he requests another seats.

Currently I am marking offered seats as "locked" with a client id, and use SELECT to return them to the client (this is for MySQL, but target database is 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)

There is a problem with that: if there's only 1 seat available, it will still be marked as "locked" and I will have to release lock right away.

I am also pretty sure that there is a smarter way of doing that. What is the correct way of dealing with a task like that?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

寂寞美少年 2024-10-26 02:54:16

你说的是预订系统。我构建此类系统的方法是有一个预订表和座位表。

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 表,现在该席位已被永久占用。

What you are discussing is a reservation system. The way I've built such systems is to have a table of reservations and table of seats.

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
    )

When someone makes a reservation you do an insert into the Reservations table with a datetime value some specified period of time in the future. When you are looking for available seats, your query looks like:

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
                    )

This allows someone to put a temporary hold on a seat if they want. If they want to purchase the seats, you insert another reservation record for some period into the future. In fact, the system I designed inserted a new reservation on each step of the purchase process that was 10 minutes into the future just to help the user finish the purchase process before the reservation expired. Once they complete the purchase you update the EventSeats table with their information and now this seat is permanently taken.

情绪操控生活 2024-10-26 02:54:16

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

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

一般来说,锁定然后等待用户响应是一种不好的技术。如果用户离开去洗澡等怎么办...那么您就会留下很多锁定的行。

看来你有两个选择:

  1. 不要锁定任何东西,如果用户尝试选择一个座位,但后来座位已售完,只需道歉并向他们展示其他一些可用座位。另外,统计这种情况发生的频率,如果您发现这种情况发生得太频繁,您可以考虑锁定方案。

  2. 按照您在问题中描述的操作,并制定一些规则,即座位预订在 2 分钟后过期等...这样您就不必担心显式释放锁,您只需检查它们的时间戳即可已设置。

You can use a SELECT ... FOR UPDATE which will lock those rows for you -- you can then figure out how many rows you've selected and if there are enough you can update them with the lock value and timestamp. If you no longer want these rows you can ROLLBACK to release the locks.
http://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE

However these only work for the duration of a transaction, if the transaction is lost, these locks will be released, so you can't use the SELECT ... FOR UPDATE lock to hold the row open, you'll need to mark them as reserved somehow.

In general locking and then waiting on a user response is a bad technique. What if the user leaves to take a shower etc... Then you are left with lots of locked rows.

It seems you have two options:

  1. Don't lock anything, if the user tries to select a seat and that is later sold out, just apologize and present them with some other available seats. Also, keep statistics on how often this happens and if you find it happens too often you can consider a locking scheme.

  2. Do what you describe in your question, and make some rule that seat reservations expire after 2 minutes etc... This way you don't have to worry about explicitly releasing the locks, you can just check the timestamps of when they were set.

一杯敬自由 2024-10-26 02:54:16

竞争条件 - 我认为这作为插入而不是更新会更好。两个更新可以同时运行,并且不会相互冲突。如果您有“锁定座位”表,那么您可以引用 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)

因此,这会为每个至少有两个座位的活动“锁定”两个座位:)

Race Condition - I think this would be better as an insert rather than an update. Two updates can run at the same time and they will not conflict with each other. If you had 'locked seats' table then you could reference the seat_id and make it unique. That way a race conditions would fail. But, in any case, I wrote this as an update as you have in the question although you could change it to an insert.

It seems like you just do not want to be able to lock the seats in the first place if there are not enough available. This is easy with self joins:

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)

So this 'locks' two seats for every event that has at least two seats :)

二货你真萌 2024-10-26 02:54:16

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

整个用例可以稍微改变(我正在考虑这里发生的票务预订) -

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

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

Just an alternative - what is the implication if you just 'let' the next customer also buy the seats and give a error popup to the first user, in case he selects those seats after the first customer has bought them?

The entire use case can be changed somewhat (I am thinking about ticket booking as it happens here) -

Customer selects movie - show timings - is shown a list of all empty seats, updated in realtime - with color coding. Then depending on which seats are selected by the customer, those are taken up for payment.

The locking mechanism you are using will always show more seats as sold out than they actually are - which can unnecessarily lead to lost sales. On the other hand, if you just have a simple check when the user actually buys the seats, to check whether the seats have been sold to someone else during the time between retrieving those seats and booking them, then you can always show an error message. Even above, after the customer selects the seats till payment, it is necessary to lock them; but then you won't face the problem of the system selecting the seats, its the customer who chooses them!

半枫 2024-10-26 02:54:16

在事务中执行更新/选择语句。如果只返回一行,则回滚事务,锁将被恢复。

Do your update/select statements in a transaction. If you only get one row back, roll back the transaction and the locks will be reverted.

初懵 2024-10-26 02:54:16

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

进行以下事务(当然是伪代码)

<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>

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

另一个更合理的方法恕我直言是以下伪代码。我认为这是一个很好的方法,因为它是无状态的,并且您不会锁定记录(使用数据库锁)等待用户决定(应该不惜一切代价避免)

 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>

问候
阿米尔

If I have understood your problem correctly, I think a solution could be the following:

make the following transaction(it's in pseudo-code of course)

<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>

You release table locks quickly this way. Then if user doesn't want the their order you
can cancel with a simple update. Also locking index doesn't prevent other updates

Another more rational approach IMHO is the following pseudo-code. I think it's a good approach since it's stateless and you don't keep the records locked(using DB locks) waiting for user to decide(which should be avoided at all costs)

 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>

Regards
Amir

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文