选择适当的锁定:

时间:2011-10-20 04:49:30

标签: sql database oracle locking

假设我有一张表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_price1

如果我在两个不同的会话中执行此操作,则会出现问题:

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)线程A插入('alice','裤子','30',1) (6)线程B插入('bob','裤子','20',1)。

我们违反了我们的不变量。

所以我意识到我可以通过执行以下操作在select之前的第一行中锁定整个表:

lock table T in exclusive mode;

如果我理解正确,那将意味着在线程A完成之前将停止对表的任何读取或写入(即线程A和线程B不能并行运行)。

除了锁定整个表格之外,还有其他方法吗? select ... for update有帮助吗?或者还有其他方法可以做更好的颗粒锁定吗?

如果这有任何区别,我会使用Oracle 10g。

2 个答案:

答案 0 :(得分:2)

是的,它的竞争条件允许两个参赛作品都认为它们的价格最优惠。您要么想出一个复杂的场景来使用唯一索引(在这种情况下第二个插入将在提交时失败)或重新考虑是否应该存储此标志。

虽然这是一个很好的旗帜,但是也有点不对劲。

想象一下,我在项目上插入了一个削弱现有价格的行,代码块中的逻辑没有将其他行is_best_price设置为0 - 它只是决定要添加的行上做什么,所以我无论竞争条件如何,都可以获得两行,同一项为1。

如果您负担得起处理开销,请在需要时计算哪个是最好的,而不是在您继续时尝试维护这些值。

答案 1 :(得分:1)

您的插入过程不会为任何其他行取消设置is_best_price标志。因此,无论如何,它都不会保持你的不变量。在我们开始谈论解决竞争条件之前,我们需要解决这个问题。在这种情况下,这样做将引导我们找到竞争条件的解决方案。

有几种方法可以执行此更新。当你将is_best_price设置为1(在ELSIF和ELSE之间)时,一个选项是将所有其他选项留空,但这不会处理关系。

另一种选择是这样做:

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

这可以在IF部分之后的任何地方完成。 (尽管上面的代码要求通过在运行UPDATE语句之前将v_best_price设置为当前项的价格来处理v_best_price为NULL的情况。)

一旦你运行它,你可以像以前一样在INSERT之后轻松运行它,只需插入is_best_price值为0的所有内容,从而完全删除IF部分。 (也就是说,将最后一行更改为:insert into T values (p_supplier, p_item, p_price, 0);)这样做会删除之前的NULL情况,因为您知道您总是至少有一个项目(除非价格可以为NULL,但在这种情况下,这个只会导致is_best_price为任何NULL定价商品的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会在表同时没有更改的情况下运行两次,但是在旧方案的任何更正版本的情况下也会发生这种情况,所以这是一个严格的改进。