Рассадка в условиях конкуренции

Shr
Дата: 07.05.2012 11:06:29
Здравствуйте.
Есть задача - рассадить (зарегистрировать и назначить места в зале) приходящих на мероприятие около 1000 людей без коллизий. Регистрация будет проходить не нескольких компьютерах одновременно, и регистрируемый имеет право попросить посадить себя рядом с несколькими другими людьми. Выбор, куда именно человека посадить - не предоставляется.
Возможные места для рассадки содержатся в таблице place (id, sector, row, col, is_busy, is_closed).
Список людей к посадке (точнее, их билетов) - содержится во временной таблице, заполняемой вместе с регистрацией нового или существующего билета. Также, имеет место предварительная регистрация, т.е. билеты с уже заполненными местами.
После создания или регистрации существующих билетов и их объединения для посадки рядом (добавления записей в gtt_tickets_to_land) буду вызывать процедуру рассадки и после ее выполнения незамедлительно делать commit.

Процедуру рассадки написал, отладил, теперь прошу комментариев сообщества о том, что сделано неправильно.
База - firebird 2.5.1, параметры пишущей транзакции - write,nowait,rec_version,read_committed.

Таблицы:
place (event - мероприятие, sector - сектор, row - ряд, col - место в ряду) - все существующие места в зале. предполагается, что дырок в местах одного ряда нет.
ticket (event - мероприятие, employee - человек, place - место, ссылка на place.id) - билеты на мероприятие
gtt_tickets_to_land (ticket - ссылка на ticket.id) - билеты, собранные для посадки вместе

create or alter procedure CRB_LAND_TICKETS
AS
  declare variable l_event id_domain;
  declare variable l_tickets_count integer;
  declare variable l_sector id_domain = 0;
  declare variable l_done integer = 0;
  declare variable l_row integer = 0;
  declare variable l_col integer = 0;
  declare variable l_loop_counter integer = 0;
begin
  -- сколько билетов надо посадить
  begin
    select t.event, count(*)
      from gtt_tickets_to_land tl
      join ticket t on t.id = tl.ticket
      group by t.event
      into :l_event, :l_tickets_count;
  when
    -- если в разных мероприятиях
    gdscode sing_select_err do
      exception e_tickets_different_event;
  end

/*
  - в цикле ищем подходящие записи и пытаемся их записать
  - если ошибка блокировки или записано меньше чем нужно записей то идем на след. итерацию
*/
  while (l_done = 0 and l_loop_counter < 1000) do
  begin
    -- ищем подходящие места (l_tickets_count подряд идущих)
    select first 1 p.sector, p.row, p.col
      from place p
      -- пропускаем закрытые сектора
      join sector s on s.id = p.sector and s.is_closed = 0
      where
        p.event = :l_event and
        p.is_busy = 0 and
        p.is_closed = 0 and
        -- после заблокированных, т.е. не подошедших в прошлый ра
        (
          p.sector > :l_sector or
          (p.sector = :l_sector and p.row > :l_row) or
          (p.sector = :l_sector and p.row = :l_row and p.col > :l_col) or
          1=0
        ) and
        (
          select count(*)
          from place p2
          where
            p2.event = :l_event and
            p2.sector = p.sector and
            p2.row = p.row and
            p2.col between p.col and p.col + :l_tickets_count - 1 and
            p2.is_closed = 0 and
            p2.is_busy = 0 and
            1=1
        ) = :l_tickets_count and
        1=1
      order by p.sector, p.row, p.col
      into :l_sector, :l_row, :l_col;
    
    -- пытаемся взять их себе
    begin
      update place p
        set p.is_busy = 1
        where
          p.event = :l_event and
          p.sector = :l_sector and
          p.is_busy = 0 and
          p.is_closed = 0 and
          p.row = :l_row and
          p.col between :l_col and :l_col + :l_tickets_count - 1 and
          1=1
      ;
      -- если не были заняты после пред. select-а
      if (row_count = :l_tickets_count) then
        l_done = 1;
    when
      -- если заняты по блокировке, ищем дальше
      gdscode lock_conflict,
      gdscode lock_timeout,
      gdscode deadlock
    do
      begin

      end
    end
    l_loop_counter = l_loop_counter + 1;
  end

  -- сажаем на выбранные места
  if (l_done = 1) then
    merge into ticket t
      using (
        select tl.ticket, p.id place
        from gtt_tickets_to_land tl
        join place p on
          p.event = :l_event and
          p.sector = :l_sector and
          p.row = :l_row and
          p.col between :l_col and :l_col + :l_tickets_count - 1 and
          1=1
      ) v
      on (v.ticket = t.id)
      when matched then
        update set
          t.place = v.place
    ;
  else
    exception e_cannot_land_tickets;
end
Shr
Дата: 07.05.2012 19:35:12
Уже вижу первую ошибку.
Если update изменит неправильное количество записей, и цикл пойдет на след. итерацию, то после commit-а мы займем лишние места. Как это обходить - делать проверку на холостом update и только потом настоящий? Этот холостой update может добавить блокировок для тех записей, которые мы могли бы и не трогать - но это, вроде, не критично.

В идеале, конечно, сделать бы select ... with lock для нужных записей и выкинуть цикл, но что-то у меня этот запрос не получается написать.
vvm
Дата: 07.05.2012 22:37:17
Shr,

вопросов нет.
Shr
Дата: 08.05.2012 08:17:08
Нашел еще косяк. В merge нет условия соединения таблиц в исходном запросе, поэтому он выдает l_tickets_count^2 записей, и update каждой записи в ticket делается l_tickets_count же раз. Разрешил, добавив поле с порядковым номером в gtt_tickets_to_land. Заодно получаем возможность выбирать порядок посадки.

Оракл в таких случаях выдает ошибку ORA-30926.