Блокировка строк в таблице для SELECT и UPDATE - PullRequest
5 голосов
/ 24 февраля 2011

Я пишу сценарий, который должен забронировать места в кинотеатре.

  1. Пользователь запрашивает 2 места
  2. Если доступно 2 места, система предлагает их клиенту
  3. Клиент может либо принять их, либо запросить еще 2 места.
  4. Когда он, наконец, принимает, места помечаются как «проданные»

Поскольку может быть несколько пользователей, использующихОдновременно с этим мне нужен способ «заблокировать» ряды, предлагаемые текущему клиенту, до тех пор, пока не пройдет определенное время, или он запросит другие места.

В настоящее время я отмечаю предложенные места как «заблокированные» с помощью идентификатора клиента, ииспользуйте 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 место, оно все равно будет помечено как "заблокирован ", и мне придется немедленно снять блокировку.

Я также почти уверен, что есть более разумный способ сделать это.Как правильно справиться с такой задачей?

Ответы [ 6 ]

3 голосов
/ 24 февраля 2011

То, что вы обсуждаете, это система бронирования. Способ, которым я построил такие системы, состоит в том, чтобы иметь стол резервирования и стол мест.

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, указав их информацию, и теперь это место занято навсегда.

2 голосов
/ 24 февраля 2011

Вы можете использовать SELECT ... FOR UPDATE, который будет блокировать эти строки для вас - тогда вы сможете выяснить, сколько строк вы выбрали, и, если их достаточно, вы можете обновить их с помощью значения блокировки и метки времени.Если вам больше не нужны эти строки, вы можете ROLLBACK снять блокировки.http://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE

Однако они работают только на время транзакции, если транзакция потеряна, эти блокировки будут сняты, поэтому вы не можете использовать блокировку SELECT ... FOR UPDATE, чтобы держать строку открытой, выНужно как-то пометить их как зарезервированные.

В общем, блокировка, а затем ожидание ответа пользователя - плохая техника.Что, если пользователь уйдет принять душ и т. Д. Тогда у вас останется много заблокированных рядов.

Кажется, у вас есть два варианта:

  1. Дон 'Если кто-то пытается выбрать место, которое позже распродается, просто извинитесь и подарите ему другие доступные места.Кроме того, ведите статистику о том, как часто это происходит, и если вы обнаружите, что это происходит слишком часто, вы можете рассмотреть схему блокировки.

  2. Делайте то, что вы описываете в своем вопросе, и сделайте какое-то правило, которое занимает местосрок действия резервирования истекает через 2 минуты и т. д. Таким образом, вам не нужно беспокоиться о явном снятии блокировок, вы можете просто проверить отметки времени, когда они были установлены.

0 голосов
/ 24 февраля 2011

Состояние гонки - Я думаю, что это будет лучше в качестве вставки, чем обновления. Два обновления могут выполняться одновременно, и они не будут конфликтовать друг с другом. Если у вас есть таблица «заблокированных мест», вы можете сослаться на 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)

Таким образом, это «блокирует» два места для каждого события, которое имеет как минимум два места:)

0 голосов
/ 24 февраля 2011

Обновите / выберите операторы в транзакции.Если вы вернули только одну строку, откатите транзакцию, и блокировки будут отменены.

0 голосов
/ 24 февраля 2011

Просто альтернатива - какой смысл, если вы просто «дадите» следующему покупателю также купить места и выдадите сообщение об ошибке первому пользователю, если он выберет эти места после того, как первый покупатель их купил?

Весь вариант использования может быть несколько изменен (я думаю о бронировании билетов, как это происходит здесь) -

Клиент выбирает фильм - показывать время - отображается список всех свободных мест, обновляемый в реальном времени - с цветовой кодировкой. Затем, в зависимости от того, какие места выбраны клиентом, они принимаются к оплате.

Используемый вами механизм блокировки всегда будет показывать больше проданных мест, чем на самом деле, что может привести к потере продаж. С другой стороны, если у вас есть простая проверка, когда пользователь фактически покупает места, чтобы проверить, были ли места проданы кому-либо еще в период между получением этих мест и их бронированием, тогда вы всегда можете показать сообщение об ошибке. , Даже выше, после того, как клиент выбирает места до оплаты, необходимо заблокировать их; но тогда вы не столкнетесь с проблемой выбора мест, а клиент, который их выберет!

0 голосов
/ 24 февраля 2011

Если я правильно понял вашу проблему, думаю, что решение может быть следующим:

совершить следующую транзакцию (конечно, в псевдокоде)

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

С уважением Amir

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...