假设我有一张表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)线程A插入('alice','裤子','30',1)
(6)线程B插入('bob','裤子','20',1)。
我们违反了我们的不变量。
所以我意识到我可以通过执行以下操作在select之前的第一行中锁定整个表:
lock table T in exclusive mode;
如果我理解正确,那将意味着在线程A完成之前将停止对表的任何读取或写入(即线程A和线程B不能并行运行)。
除了锁定整个表格之外,还有其他方法吗? select ... for update
有帮助吗?或者还有其他方法可以做更好的颗粒锁定吗?
如果这有任何区别,我会使用Oracle 10g。
答案 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会在表同时没有更改的情况下运行两次,但是在旧方案的任何更正版本的情况下也会发生这种情况,所以这是一个严格的改进。