SELECT ... FOR UPDATE是否应该包含ORDER BY?

时间:2012-07-03 13:26:09

标签: mysql oracle locking deadlock

假设我们执行......

SELECT * FROM MY_TABLE FOR UPDATE

... MY_TABLE中有多行。

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

  • 交易1:锁定A行。
  • 交易2:锁定B行。
  • 事务1:尝试锁定行B和块。
  • 事务2:尝试锁定A行和死锁。

解决此问题的方法是使用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选择不同的执行计划,并且可能以不同的顺序获取行)。

这说明当从光标中获取行时确实完成了锁定(当光标打开时,不会立即对整个结果集进行锁定)。

3 个答案:

答案 0 :(得分:3)

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

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

在实践中,这种情况非常特殊,不应该经常发生:如果你担心死锁,我仍然认为有更简单的方法来阻止它们。

防止死锁的最简单方法是使用FOR UPDATE NOWAITFOR 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.

这清楚地表明了

  1. 第一个会话已经锁定了整个结果集,因为第二个会话永远不会锁定该行,即使第一个会话尚未检索到它。
  2. 即使第二个会话无法更新任何内容,也会引发Deadlock detected异常。 1.即使第一个会话没有更新任何获取的哇,也会引发Deadlock detected异常。
  3. 代码很容易修改,以演示FOR UPDATE变体的不同行为。