我正在编写一个需要预订电影院座位的剧本。
由于可能有多个用户同时使用系统,我需要一种方法来“锁定”提供给当前客户端的行,直到某个时间过去,或者他请求另一个席位。
目前,我将提供的座位标记为“已锁定”客户端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)
有一个问题:如果只有一个座位可用,它仍会被标记为“已锁定”,我将立即释放锁定。
我也很确定有更聪明的方法。处理这样的任务的正确方法是什么?
答案 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
锁来保持行打开,您需要将它们标记为保留某种方式。
通常,锁定然后等待用户响应是一种糟糕的技术。如果用户离开洗澡等等,那么你会留下许多锁定的行。
看来你有两个选择:
不要锁定任何东西,如果用户试图选择一个座位并且稍后售罄,只需道歉并将其与其他可用座位一起展示。此外,请记录这种情况发生的频率,如果您发现它经常发生,您可以考虑使用锁定方案。
执行您在问题中描述的内容,并制定一些规则,让座位预订在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)
因此,对于每个至少有两个座位的活动,这个'锁定'两个席位:)