15

假设我们执行...

SELECT * FROM MY_TABLE FOR UPDATE

...而且 MY_TABLE 中不止一行。

从理论上讲,如果两个并发事务执行此语句,但它恰好以不同的顺序遍历(并因此锁定)行,则可能会发生死锁。例如:

  • 事务 1:锁定 A 行。
  • 事务 2:锁定 B 行。
  • 事务 1:尝试锁定 B 行和块。
  • 事务 2:尝试锁定 A 行并出现死锁。

解决这个问题的方法是使用 ORDER BY 来确保行总是以相同的顺序锁定。

所以,我的问题是:这种理论上的僵局会在实践中发生吗?我知道有办法人为诱导它,但它会在正常操作中发生吗?我们应该总是使用 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 实例),运行相同的块,但DESCORDER 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 选择不同的执行计划并可能以不同的顺序获取行)。

这说明锁定确实是在从游标中获取行时完成的(而不是在打开游标时立即针对整个结果集)。

4

2 回答 2

4

您在问题中的示例表明锁定顺序取决于访问方法。这个访问路径不是由查询的 ORDER BY 子句直接决定的,影响这个访问路径的因素有很多。因此,您不能仅通过添加 ORDER BY 来防止死锁,因为您仍然可以有两个不同的访问路径。事实上,通过使用 order by 运行您的测试用例并更改会话参数,我能够使两个会话运行到具有相同查询的 ORA-60 中。

如果所涉及的会话没有其他挂起的锁,则在所有会话中以相同的顺序锁定行将防止死锁,但是如何可靠地强制执行此顺序?请注意,这只会防止这种非常特殊的死锁情况。在每个会话或不同的计划中,您仍然可能会遇到多个查询的死锁。

实际上,这种情况非常特殊,无论如何都不应该经常发生:如果您担心死锁,我仍然认为有更简单的方法可以防止它们。

防止死锁的最简单方法是使用FOR UPDATE NOWAITor FOR UPDATE WAIT X(尽管 WAIT X 仍然可以触发具有优于死锁检测机制的 X 值的死锁,我相信目前为 11g 的 3 秒——感谢@APC的更正)。

换句话说,两个事务都应该问:给我这些行并锁定它们,但是如果另一个用户已经有一个锁,则返回一个错误,而不是无限期地等待。导致死锁的是无限期的等待。

在实践中,我会说大多数具有真人用户的应用程序宁愿立即收到错误,也不愿让事务无限期地等待另一个事务完成。我只考虑FOR UPDATENOWAIT关键批处理作业。

于 2012-07-03T13:39:08.823 回答
2

我认为您误解了 FOR UPDATE 的工作原理。它在游标被激活时获取锁;也就是说,当发出 SELECT 时。

因此,运行您的查询,事务 1 将锁定整个表(因为您没有指定 WHERE 子句)。无论事务 1 是否针对选定的记录集发布了任何 DML,事务 2 都将挂起或失败(取决于您在 WAIT 子句中指定的内容)。事实上,事务 1 甚至不需要获取任何记录;一旦事务 1 打开 FOR UPDATE 游标,事务 2 将抛出 ORA-00054。

您描述的死锁场景是使用乐观锁定的应用程序的经典结果(即假设它能够在需要时获取锁)。FOR UPDATE 的全部意义在于它是一种悲观的锁定策略:抓住现在可能需要的所有锁,以保证将来成功处理。


不可估量的 Kyte 先生在他的博客中提供了至关重要的见解

“死锁检测胜过等待期”

在我的代码中,我在第二个会话中使用的游标的 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。如果他们指定了一个较短的等待期——比如 WAIT 1——他们就会看到ORA-30006: resource busy

请注意,无论我们是否使用详细语法,都会发生这种情况......

open c10000;
loop
    fetch c10000 into r; 

或者更时髦的......

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.

这清楚地表明了

  1. 第一个会话已从 go-get 锁定整个结果集,因为第二个会话永远不会锁定该行,即使第一个会话尚未检索它也是如此。
  2. 即使第二Deadlock detected个会话无法更新任何内容,也会引发异常。1.Deadlock detected即使第一个会话没有更新任何获取的 wow,也会抛出异常。

该代码很容易修改以演示 FOR UPDATE 变体的不同行为。

于 2012-07-03T14:37:01.550 回答