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

Bra*_*vic 15 mysql oracle deadlock locking

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

SELECT * FROM MY_TABLE FOR UPDATE
Run Code Online (Sandbox Code Playgroud)

...... 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;
Run Code Online (Sandbox Code Playgroud)

...从一个客户端会话(我使用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;
/
Run Code Online (Sandbox Code Playgroud)

另一个客户端会话(我只是启动了一个SQL Developer实例),运行相同的块,但DESCORDER BY.几秒钟后,你会得到:

ORA-00060: deadlock detected while waiting for resource
Run Code Online (Sandbox Code Playgroud)

顺便说一句,你可能会通过完全删除ORDER BY(所以两个块都相同),并添加...来实现相同的结果.

ALTER SESSION SET OPTIMIZER_INDEX_COST_ADJ = 1;
Run Code Online (Sandbox Code Playgroud)

......在一个街区前......但......

ALTER SESSION SET OPTIMIZER_INDEX_COST_ADJ = 10000;
Run Code Online (Sandbox Code Playgroud)

...在另一个之前(因此Oracle选择不同的执行计划,并且可能以不同的顺序获取行).

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

Vin*_*rat 4

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

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

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

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

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

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