Выбор Oracle для обновления

Проблема, которую мы пытаемся решить, выглядит так.

  • У нас есть таблица, полная строк, представляющих карты. Цель транзакции резервирования - назначить карту клиенту.
  • Карта не может принадлежать многим клиентам
  • Через некоторое время (если он не куплен) карта должна быть возвращена в пул доступных возобновлений.
  • Бронирование может выполняться одновременно несколькими клиентами.
  • Мы используем базу данных Oracle для хранения данных, поэтому решение должно работать как минимум на Oracle 11

Нашим решением является присвоение статуса карте и сохранение ее даты резервирования. При резервировании карты мы делаем это, используя инструкцию "select for update". Запрос ищет доступные карты и карты, которые были зарезервированы давно.

Однако наш запрос не работает должным образом.

Я подготовил упрощенную ситуацию, чтобы объяснить проблему. У нас есть таблица card_numbers, полная данных - все строки имеют ненулевые номера идентификаторов. Теперь попробуйте заблокировать некоторые из них.

-- first, in session 1
set autocommit off;

select id from card_numbers  
where id is not null  
and rownum <= 1  
for update skip locked;

Мы не совершаем транзакцию здесь, строка должна быть заблокирована.

-- later, in session 2
set autocommit off;

select id from card_numbers  
where id is not null  
and rownum <= 1  
for update skip locked;

Ожидаемое поведение заключается в том, что в обоих сеансах мы получаем одну, другую строку, которая удовлетворяет условиям запроса.

Однако это не работает. В зависимости от того, используем ли мы "пропущенную блокировку" часть запроса или нет - поведенческие изменения:

  • без "пропустить блокировку" - второй сеанс заблокирован - ожидание транзакции фиксации или откат в сеансе 1
  • с "skip locked" - второй запрос возвращает сразу пустой набор результатов

Итак, после этого длинного введения возникает вопрос.

Возможно ли такое поведение блокировки в Oracle? Если да, то что мы делаем неправильно? Каким будет правильное решение?

Ответы

Ответ 1

Поведение, с которым вы столкнулись для FOR UPDATE SKIP LOCKED, описано в этой заметке в блоге. Я понимаю, что предложение FOR UPDATE оценивается ПОСЛЕ предложения WHERE. SKIP LOCKED похож на дополнительный фильтр, который гарантирует, что среди строк, которые были бы возвращены, ни один из них не заблокирован.

Ваше утверждение логически эквивалентно: найдите первую строку из card_numbers и верните ее, если она не заблокирована. Очевидно, это не то, что вы хотите.

Вот небольшой тестовый пример, который воспроизводит описанное вами поведение:

SQL> CREATE TABLE t (ID PRIMARY KEY)
  2  AS SELECT ROWNUM FROM dual CONNECT BY LEVEL <= 1000;

Table created

SESSION1> select id from t where rownum <= 1 for update skip locked;

        ID
----------
         1

SESSION2> select id from t where rownum <= 1 for update skip locked;

        ID
----------

Никакая строка не возвращается со второго выбора. Вы можете использовать курсор, чтобы обойти эту проблему:

SQL> CREATE FUNCTION get_and_lock RETURN NUMBER IS
  2     CURSOR c IS SELECT ID FROM t FOR UPDATE SKIP LOCKED;
  3     l_id NUMBER;
  4  BEGIN
  5     OPEN c;
  6     FETCH c INTO l_id;
  7     CLOSE c;
  8     RETURN l_id;
  9  END;
 10  /

Function created

SESSION1> variable x number;
SESSION1> exec :x := get_and_lock;

PL/SQL procedure successfully completed
x
---------
1

SESSION2> variable x number;
SESSION2> exec :x := get_and_lock;

PL/SQL procedure successfully completed
x
---------
2

Поскольку я явно выбрал курсор, будет возвращена только одна строка (и будет заблокирована только одна строка).

Ответ 2

В то время как другие ответы уже достаточно объяснили, что происходит в вашей базе данных с различными вариантами SELECT .. FOR UPDATE, я думаю, стоит упомянуть, что Oracle не рекомендует использовать FOR UPDATE SKIP LOCKED напрямую и рекомендует вместо этого использовать Oracle AQ:

http://download.oracle.com/docs/cd/B28359_01/server.111/b28286/statements_10002.htm#i2066346

Мы используем Oracle AQ в нашем приложении, и я могу подтвердить, что после некоторой крутой кривой обучения он может быть довольно удобным способом обращаться с производителями/потребителями непосредственно в базе данных

Ответ 3

Не то, чтобы ответ Винсента был неправильным, но я бы разработал его по-другому.

Мой первый инстинкт - выбрать для обновления первой доступной записи и обновить запись с помощью "reserved_date". После того, как прошло XXX и транзакция не была завершена, обновите запись reserved_date до нулевого значения, освободив запись снова.

Я стараюсь держать все как можно проще. Для меня это проще.