【问题标题】:Can a deadlock occur with the same access method?使用相同的访问方法会发生死锁吗?
【发布时间】:2012-02-09 17:55:29
【问题描述】:

如果两个并发的DML语句修改相同的数据并使用相同的访问方法,是否可能发生死锁?

根据我的测试以及我对 Oracle 工作原理的猜测,答案是否定的。

但我想 100% 确定。我正在寻找表明死锁不会以这种方式发生的官方消息来源,或者是证明死锁可以以这种方式发生的测试用例。


问这个问题的另一种方式是:如果使用相同的访问方法,Oracle 是否总是以相同的顺序返回结果? (并且运行之间没有数据变化。)

例如,如果查询使用全表扫描并以 4/3/2/1 的顺序返回行,它会总是按该顺序返回行吗?如果索引范围扫描以 1/2/3/4 的顺序返回行,它会总是按该顺序返回行吗?实际顺序是什么并不重要,顺序是确定性的。

(并行性可能会给这个问题增加一些复杂性。语句的整体顺序会因许多因素而异。但是对于锁定,我相信只有每个并行会话中的顺序很重要。再说一次,我的测试表明顺序是确定性的,不会导致死锁。)


更新

我最初的问题有点笼统。我最感兴趣的是,是否可以同时在两个不同的会话中运行update table_without_index set a = -1 之类的东西,然后陷入僵局? (我问的是单个更新,而不是一系列更新。)

首先,让我证明完全相同的语句会导致死锁。

创建表、索引和一些数据:

为简单起见,我只更新同一列。在现实世界中会有不同的列,但我认为这不会改变任何事情。

请注意,我使用 pctfree 0 创建表,更新后的值会占用更多空间,因此会有大量行迁移。 (这是对@Tony Andrew 的回答,尽管我担心我的测试可能过于简单。另外,我认为我们不需要担心在更新之间插入行;只有一个更新会看到新行,所以它不会导致死锁。除非新行也移动了一堆其他东西。)

drop table deadlock_test purge;

create table deadlock_test(a number) pctfree 0;
create index deadlock_test_index on deadlock_test(a);

insert into deadlock_test select 2 from dual connect by level <= 10000;
insert into deadlock_test select 1 from dual connect by level <= 10000;

commit;

在会话 1 中运行此块:

begin
    while true loop
        update deadlock_test set a = -99999999999999999999 where a > 0;
        rollback;
    end loop;
end;
/

在会话 2 中运行此块:

--First, influence the optimizer so it will choose an index range scan.
--This is not gaurenteed to work for every environment.  You may need to 
--change other settings for Oracle to choose the index over the table scan.
alter session set optimizer_index_cost_adj = 1;

begin
    while true loop
        update deadlock_test set a = -99999999999999999999 where a > 0;
        rollback;
    end loop;
end;
/

几秒钟后,其中一个会话抛出 ORA-00060: deadlock detected while waiting for resource。这是因为同一查询在每个会话中以不同的顺序锁定行。

排除上述情况,是否会发生死锁?

以上演示了执行计划的更改可能导致死锁。 但是即使执行计划保持不变,也会发生死锁吗?

据我所知,如果您删除optimizer_index_cost_adj 或其他任何会改变计划的内容,代码将永远不会导致死锁。 (我已经运行了一段时间的代码,没有任何错误。)

我问这个问题是因为我正在开发的系统偶尔会发生这种情况。它还没有失败,但我们想知道它是否真的安全,或者我们是否需要在更新周围添加额外的锁定?

有人可以构建一个测试用例,其中单个更新语句同时运行并使用相同的计划会导致死锁吗?

【问题讨论】:

  • 不确定关于 DML 死锁的第一部分与关于查询结果排序的第二部分有何相同之处,但对于问题 2,您需要在查询中使用“ORDER BY”以保证行按特定顺序排列。
  • @tbone 我希望我的更新使连接更清晰。 (这是一个复杂的问题,希望我解释的准确。)如果两个查询尝试同时更新表中的所有行,但顺序不同,则可能会出现死锁。当一个查询使用索引而另一个查询使用 FTS 时,很容易看到这种情况。但它可以仅通过 FTS 或仅索引范围扫描发生吗?这取决于这些操作是否以相同的顺序处理行。

标签: oracle


【解决方案1】:

仅当您在查询中包含 ORDER BY 时,从您的角度来看,“顺序”是确定性的从服务器的角度来看是否是确定性是一个实现细节,不能依赖。

至于锁定,两个相同的 DML 语句可以相互阻塞(但不会死锁)。例如:

CREATE TABLE THE_TABLE (
    ID INT PRIMARY KEY
);

交易A:

INSERT INTO THE_TABLE VALUES(1);

事务 B:

INSERT INTO THE_TABLE VALUES(1);

此时,事务 B 停滞,直到事务 A 提交或回滚。如果 A 提交,则 B 由于 PRIMARY KEY 违规而失败。如果 A 回滚,则 B 成功。

可以为 UPDATE 和 DELETE 构造类似的示例。

重要的一点是阻塞将不依赖于执行计划——无论 Oracle 选择如何优化您的查询,您将始终拥有相同的阻塞行为。您可能想阅读有关Automatic Locks in DML Operations 的更多信息。


至于-locks,它们可以通过多个语句来实现。例如:

A: INSERT INTO THE_TABLE VALUES(1);
B: INSERT INTO THE_TABLE VALUES(2);
A: INSERT INTO THE_TABLE VALUES(2);
B: INSERT INTO THE_TABLE VALUES(1); -- SQL Error: ORA-00060: deadlock detected while waiting for resource

或者,可能使用以不同顺序修改多行的语句以及一些非常不走运的时间(有人可以证实这一点吗?)。

--- 更新 ---

针对你的问题的更新,让我做一个一般性的观察:如果执行的并发线程以一致的顺序锁定对象,死锁是不可能的。对于任何类型的锁定都是如此,无论是普通多线程程序中的互斥锁(例如,参见Herb Sutter's thoughts on Lock Hierarchies)还是数据库。一旦您更改顺序以使任意两个锁“翻转”,就会引入死锁的可能性。

在不扫描索引的情况下,您正在以一种顺序更新(和锁定)行,而索引则以另一种顺序进行。所以,这可能就是你的情况:

  • 如果您对两个并发事务禁用索引扫描,它们都会以相同的顺序 [X] 锁定行,因此不会出现死锁。
  • 如果您仅对一个事务启用索引扫描,它们将不再以相同的顺序锁定行,因此可能会出现死锁。
  • 如果您对两个事务都启用索引扫描,那么它们再次以相同的顺序锁定行,并且死锁是不可能的(继续在两个会话中尝试 alter session set optimizer_index_cost_adj = 1;,然后您会看到)。

[X] 虽然我不会依赖具有保证顺序的全表扫描 - 这可能只是当前 Oracle 在这些特定情况下的工作方式,而某些未来的 Oracle 或不同情况可能会产生不同的行为。

因此,索引的存在是偶然的 - 真正的问题是排序。碰巧 UPDATE 中的排序可能会受到索引的影响,但如果我们可以以另一种方式影响排序,我们会得到类似的结果。

由于 UPDATE 没有 ORDER BY,因此您无法真正保证仅通过 UPDATE 锁定的顺序。但是,如果您将锁定与更新分开,那么您可以保证锁定顺序:

SELECT ... ORDER BY ... FOR UPDATE;

虽然您的原始代码在我的 Oracle 10 环境中导致死锁,但以下代码不会:

第 1 节:

declare
    cursor cur is select * from deadlock_test where a > 0 order by a 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;
/

第 2 节:

alter session set optimizer_index_cost_adj = 1;

declare
    cursor cur is select * from deadlock_test where a > 0 order by a 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;
/

【讨论】:

  • +1 感谢您的回复。我对您的示例进行了一些小改动,以使僵局发生。我之前没有考虑过这种插入场景,很高兴你提到了它。但这不是我主要感兴趣的。我担心“倒霉的时机”。基本上你 can 有完全相同的更新语句在某些情况下会导致死锁,我试图准确地确定何时可能。我将很快用一些示例更新我的问题,并尝试包含@Tony Andrew 关于行迁移的想法。
  • @jonearles 第二次阅读您更新的问题后,我意识到我重复了一些您显然已经知道的事情。无论如何,我会留下我更新的答案,因为它的价值。我个人不会依赖全表扫描顺序是确定性的,也不会依赖optimizer_index_cost_adj 总是被尊重。使用SELECT ... ORDER BY ... FOR UPDATE真正保证锁定顺序一致...
【解决方案2】:

返回行的顺序是不确定的。行在更新后可能会“迁移”到不同的块,在这种情况下,它将出现在全表扫描结果中的不同位置。或者(也许更有可能)在两个现有行之间插入新行。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-04
    • 2019-01-28
    • 1970-01-01
    • 2012-04-24
    • 2022-12-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多