假设我们执行......
SELECT * FROM MY_TABLE FOR UPDATE
... MY_TABLE中有多行。
理论上,如果两个并发事务执行此语句,但它恰好以不同的顺序遍历(并因此锁定)行,则可能发生死锁。例如:
解决此问题的方法是使用ORDER BY来确保始终以相同的顺序锁定行。
所以,我的问题是:这种理论上的僵局是否会在实践中发生?我知道有artificially induce it的方法,但它能否在正常运行中发生?我们应该只使用ORDER BY,还是实际上可以安全地省略它?
我主要对Oracle和MySQL / InnoDB的行为感兴趣,但对其他DBMS的评论也会有所帮助。
以下是当锁定顺序不同时,如何在Oracle下重现死锁:
创建测试表并用一些测试数据填充......
CREATE TABLE DEADLOCK_TEST (
ID INT PRIMARY KEY,
A INT
);
INSERT INTO DEADLOCK_TEST SELECT LEVEL, 1 FROM DUAL CONNECT BY LEVEL <= 10000;
COMMIT;
...从一个客户端会话(我使用SQL Developer),运行以下块:
DECLARE
CURSOR CUR IS
SELECT * FROM DEADLOCK_TEST
WHERE ID BETWEEN 1000 AND 2000
ORDER BY ID
FOR UPDATE;
BEGIN
WHILE TRUE LOOP
FOR LOCKED_ROW IN CUR LOOP
UPDATE DEADLOCK_TEST
SET A = -99999999999999999999
WHERE CURRENT OF CUR;
END LOOP;
ROLLBACK;
END LOOP;
END;
/
从不同的客户端会话(我只是启动了一个SQL Developer实例),运行相同的块,但DESC
中使用ORDER BY
。几秒钟后,你会得到:
ORA-00060: deadlock detected while waiting for resource
顺便说一下,你可能会通过完全删除ORDER BY
(因为两个块都相同)并添加......来实现相同的结果。
ALTER SESSION SET OPTIMIZER_INDEX_COST_ADJ = 1;
......在一个街区前......但是......
ALTER SESSION SET OPTIMIZER_INDEX_COST_ADJ = 10000;
...在另一个之前(因此Oracle选择不同的执行计划,并且可能以不同的顺序获取行)。
这说明当从光标中获取行时确实完成了锁定(当光标打开时,不会立即对整个结果集进行锁定)。
答案 0 :(得分:3)
您的问题中的示例显示锁定的顺序取决于访问方法。此访问路径不是由查询的ORDER BY子句直接决定的,有许多因素可以影响此访问路径。因此,只能通过添加ORDER BY来阻止死锁,因为您仍然可以拥有两个不同的访问路径。实际上,通过使用order by运行测试用例并更改会话参数,我能够使两个会话运行到具有相同查询的ORA-60中。
如果所涉及的会话没有其他锁定挂起,则在所有会话中锁定的行将阻止死锁,但如何可靠地强制执行此命令?请注意,这只会阻止这种非常特殊的死锁情况。在每个会话或不同的计划中,您仍然可能会遇到多个查询的死锁。
在实践中,这种情况非常特殊,不应该经常发生:如果你担心死锁,我仍然认为有更简单的方法来阻止它们。
防止死锁的最简单方法是使用FOR UPDATE NOWAIT
或FOR UPDATE WAIT X
(尽管WAIT X仍然可以触发死锁,其值超过死锁检测机制,目前为3秒) 11g我相信 - 感谢@APC进行更正。
换句话说,两个事务都应该询问:给我那些行并锁定它们,但如果另一个用户已经有锁,则返回错误而不是无限期地等待。这是导致死锁的无限期等待。
在实践中,我会说大多数具有真实用户的应用程序宁愿立即收到错误,而不是让事务无限期地等待另一个事务完成。对于非关键批处理作业,我会考虑FOR UPDATE
而不使用NOWAIT
。
答案 1 :(得分:2)
以另一种方式看待它。除了奇怪的实现,省略ORDER BY子句几乎确定每次产生相同的顺序,但包括它们提供了“维护”的机会。程序员和其他祝福者,以及他们中的一些,以便他们在应用程序的整个生命周期中有一些有限的概率结束。
答案 2 :(得分:1)
我认为您误解了FOR UPDATE的工作原理。它在激活光标时获取锁定;即,在发出SELECT时。
因此,运行查询时,事务1将锁定整个表(因为您尚未指定WHERE子句)。无论事务1是否针对所选记录集发出任何DML,事务2将挂起或失败(取决于您在WAIT子句中指定的内容)。事实上,事务1甚至不需要获取任何记录;一旦事务1打开FOR UPDATE游标,事务2将抛出ORA-00054。
您描述的死锁场景是使用乐观锁定的应用程序的经典结果(即假设它将能够在需要时获取锁定)。 FOR UPDATE的重点是它是一个悲观的锁定策略:抓住所有可能需要的锁 now ,以保证将来成功处理。
不可估量的Kyte先生提供the crucial insight in his blog:
“死锁检测胜过等待期”
在我的代码中,我在第二个会话中使用的游标的FOR UPDATE子句中使用了NOWAIT:
cursor c10000 is
select * from order_lines
where header_id = 1234
for update;
cursor c1 is
select * from order_lines
where header_id = 1234
and line_id = 9999
for update nowait;
因此,第2节会议立即失败,并向ORA-00054投降。
然而,OP没有指定任何内容,在这种情况下,第二个会话将无限期地等待释放该行。除了它没有,因为一段时间后死锁检测开始并以极端偏见终止命令,即ORA-00060。如果他们指定了一个短暂的等待期 - 比如等待1 - 他们会看到ORA-30006: resource busy
。
请注意,无论我们是否使用详细语法...
,都会发生这种情况open c10000;
loop
fetch c10000 into r;
或者snazzier ......
for r in c10000 loop
当会话2开始时,会话1是否已经获取了感兴趣的行并不重要。
<强> TL;博士强>
所以关键是ORDER BY没有解决任何问题。发出FOR UPDATE的第一个会话抓取结果集中的所有记录。尝试更新任何这些记录的后续会话将因ORA-00054,ORA-30006或ORA-00060而失败,具体取决于他们是否指定了NOWAIT,WAIT n 或没有....除非第一个会话在WAIT周期超时或死锁检测开始之前释放锁。
这是一个有效的例子。我正在使用自动事务来模拟第二个会话。效果相同,但输出更容易阅读。
declare
cursor c1 is
select * from emp
where deptno = 10
for update;
procedure s2
is
cursor c2 is
select * from emp
where empno = 7934 -- one of the employees in dept 10
for update
-- for update nowait
-- for update wait 1
;
x_deadlock exception;
pragma exception_init( x_deadlock, -60);
x_row_is_locked exception;
pragma exception_init( x_row_is_locked, -54);
x_wait_timeout exception;
pragma exception_init( x_wait_timeout, -30006);
pragma autonomous_transaction;
begin
dbms_output.put_line('session 2 start');
for r2 in c2 loop
dbms_output.put_line('session 2 got '||r2.empno);
update emp
set sal = sal * 1.1
where current of c2;
dbms_output.put_line('session 2 update='||sql%rowcount);
end loop;
rollback;
exception
when x_deadlock then
dbms_output.put_line('session 2: deadlock exception');
when x_row_is_locked then
dbms_output.put_line('session 2: nowait exception');
when x_wait_timeout then
dbms_output.put_line('session 2: wait timeout exception');
end s2;
begin
for r1 in c1 loop
dbms_output.put_line('session 1 got '||r1.empno);
s2;
end loop;
end;
/
在这个版本中,我在第二个会话中指定了直接for update
。这是OP使用的配置,从输出投掷中可以看出,因为已经检测到死锁:
session 1 got 7782
session 2 start
session 2: deadlock exception
session 1 got 7839
session 2 start
session 2: deadlock exception
session 1 got 7934
session 2 start
session 2: deadlock exception
PL/SQL procedure successfully completed.
这清楚地表明了
Deadlock detected
异常。
1.即使第一个会话没有更新任何获取的哇,也会引发Deadlock detected
异常。代码很容易修改,以演示FOR UPDATE变体的不同行为。