Выбор подходящей блокировки: - PullRequest
3 голосов
/ 20 октября 2011

Допустим, у меня есть таблица T со следующими столбцами:

create table T as
(
  supplier varchar2,
  item varchar2,
  price number,
  is_best_price number,
);

И у нас есть процедура для вставки элементов:

create or replace procedure insert_item
  (p_supplier as varchar2, p_item as varchar2, p_price as number) as
declare
  v_best_price;
  v_is_best_price number;
begin
  select min(price) into v_best_price from T where item = p_item;

  if (v_best_price is null) then
    v_is_best_price := 1;
  elsif (price <= v_best_price) then
    v_is_best_price := 1;
  else
    v_is_best_price := 0;
  end if;

  if (v_is_best_price = 0) then
    update T set is_best_price = 0 where item = p_item and is_best_price = 1;
  end if;

  insert into T values (p_supplier, p_item, p_price, v_is_best_price);
end;

Инвариант здесь такой, что

Forall rows x: (x.v_best_price = 1) iff (x.v_price = select min(price) from T where item = x.item)

Или, если говорить простым языком, is_best_price равно 1, если цена товара является лучшей.

Проблема возникает, если я делаю это в двух разных сессиях:

insert_item('alice', 'pants', '30');
insert_item('bob', 'pants', '20');

Теперь, если я правильно понимаю, может произойти следующее:

(1) Выполнить insert_item('alice', 'pants', '30') (нить A)
(2) Выполнить insert_item('bob', 'pants', '20') (резьба B)
(3) Поток A запрашивает таблицу, замечает, что других штанов нет, поэтому задает v_is_best_price := 1.
(4) Поток B запрашивает таблицу, замечая, что других трусов нет (поскольку поток A еще не вставлен), поэтому задает v_is_best_price := 1.
(5) Нить А вставляет («Алиса», «брюки», «30», 1).
(6) Резьба B вставляет («боб», «штаны», «20», 1).

Мы нарушили наш инвариант.

Итак, я понимаю, что могу заблокировать всю таблицу в первой строке процедуры перед выбором, выполнив следующее:

lock table T in exclusive mode;

Что, если я правильно понимаю, будет означать, что любые операции чтения или записи в таблицу будут остановлены до завершения потока A (то есть поток A и поток B не могут работать параллельно).

Есть ли другой способ сделать это, кроме блокировки всей таблицы? select ... for update помогает? Или есть какой-то другой способ сделать более точную блокировку зерна?

Я использую Oracle 10g, если это что-то меняет.

Ответы [ 2 ]

2 голосов
/ 20 октября 2011

Да, это условие гонки, которое позволяет обеим участникам считать, что они имеют лучшую цену.Вы должны либо придумать замысловатый сценарий, чтобы использовать уникальный индекс (в этом случае 2-я вставка потерпит неудачу при коммите), либо пересмотреть, следует ли вам хранить этот флаг.

Хотя это хороший флаг, чтобы иметьи удобно, это также немного неправильно.

Представьте, что я вставляю строку, которая подрезает вашу существующую цену на элемент, логика в вашем кодовом блоке не устанавливает другие строки is_best_price в 0 - это просто делаетрешение о том, что делать с добавляемой строкой, поэтому я могу получить две строки с 1 для одного и того же элемента, независимо от состояния гонки.

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

1 голос
/ 20 октября 2011

Ваша процедура вставки не отменяет флаг is_best_price для любых других строк.Итак, как написано, он не будет поддерживать ваш инвариант в любом случае.Прежде чем мы начнем говорить о решении условий гонки, мы должны это исправить.В этом случае это приведет нас к решению проблемы состояния гонки.

Существует несколько способов сделать это обновление.Один из вариантов - очистить все остальные, когда вы установите для is_best_price значение 1 (между ELSIF и ELSE), но это не будет обрабатывать связи.

Другой вариант - сделать это:

UPDATE T SET is_best_price = (price == v_best_price) WHERE item = p_item;

Это можно сделать в любом месте после части IF.(Хотя приведенный выше код требует обработки v_best_price в случае NULL, установив v_best_price в качестве цены текущего элемента перед выполнением оператора UPDATE).

Однако, запустив это, вы можете легко запуститьэто после INSERT, как и раньше, и просто вставьте все со значением is_best_price, равным 0, таким образом полностью удаляя часть IF.(То есть измените последнюю строку на: insert into T values (p_supplier, p_item, p_price, 0);). При этом удаляется случай NULL, поскольку вы знаете, что у вас всегда есть хотя бы один предмет (если цена не может быть NULL, но в этом случае это простов результате is_best_price будет равен 0 для любых элементов с нулевой ценой, включая тот, который в настоящее время вставляется).

Итак, поскольку мы дошли до этого уровня, теперь нет смысла иметь SELECT, который дает нам v_best_price вфронт функции.Это не имеет смысла, потому что мы удалили часть IF.Поэтому вместо этого мы можем просто запустить

UPDATE T SET is_best_price = (price = (SELECT MIN(price) FROM T WHERE item = p_item)) WHERE item = p_item;

. Это будет работать правильно во всех случаях.Если другой поток вставил более дешевый товар, он будет работать нормально.Если нет, то же самое.Пока UPDATE работает атомарно (что и должно), проблем не возникнет, дополнительная блокировка не требуется.Единственным недостатком этого является то, что иногда UPDATE будет запускаться дважды, если за это время таблица не изменилась, но это будет происходить в тех случаях, с любой исправленной версией вашей старой схемы, так что это строгое улучшение.

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