【问题标题】:Preventing deadlocks in SQL Server防止 SQL Server 中的死锁
【发布时间】:2017-03-27 21:23:35
【问题描述】:

我有一个连接到 SQL Server 2014 数据库的应用程序,该数据库将多行合并为一个。应用程序运行时没有与此数据库的其他连接。

首先,在特定时间范围内选择一大块行。此查询使用与集群查找合并的非集群查找(TIME 列)。

select ...
from FOO
where TIME >= @from and TIME < @to and ...

然后,我们在 c# 中处理这些行并将更改写入单个更新和多个删除,每个块会发生很多次。这些也使用非聚集索引查找。

begin tran

update FOO set ...
where NON_CLUSTERED_ID = @id

delete FOO where NON_CLUSTERED_ID in (@id1, @id2, @id3, ...)

commit

在使用多个并行块运行此程序时,我遇到了死锁。我尝试将ROWLOCK 用于updatedelete,但由于某种原因,这导致了比以前更多的死锁,即使块之间没有重叠。

然后我在update 上尝试了TABLOCKX, HOLDLOCK,但这意味着我无法并行执行我的select,因此我失去了并行性的优势。

知道如何避免死锁但仍能处理多个并行块吗?

在这种情况下,在我的select 上使用NOLOCK 是否安全,因为块之间没有行重叠?那么TABLOCKX, HOLDLOCK 只会屏蔽updatedelete,对吗?

或者我应该接受会发生死锁并在我的应用程序中重试查询?

更新(附加信息):到目前为止,所有死锁都发生在 updatedelete 阶段,select 没有发生。如果我今天无法解决这个问题,我会尝试获取一些死锁日志(之前未启用正确的跟踪标志)。

更新:这是ROWLOCK 发生的两种死锁安排,它们都只涉及delete 语句和它使用的非聚集索引。我不确定这些是否与在没有任何表提示的情况下发生的死锁相同,因为我无法重现其中任何一个。

请问.xdl 是否还需要其他任何东西,我有点厌倦了附加整个东西。

【问题讨论】:

  • 您是否尝试在选择过程中声明UPDLOCK?这样,当您更新/删除时,锁已经存在,这应该可以防止您陷入死锁。如果可能的话,与我们分享一些死锁日志记录的详细信息。
  • 你能把所有的处理都移到一个存储过程中吗?您可能也可以通过简单地打开快照隔离来解决这个问题,但这实际上取决于您在做什么。
  • @Jens 不幸的是,我现在无法获取死锁日志。似乎所有因死锁而失败的线程都处于updatedelete 阶段,因此更改select 锁不太可能影响这种情况。不幸的是,我没有任何死锁日志,我会看看是否可以为下一次尝试启用死锁跟踪标志。 @Nick.McDermaid 不幸的是不能使用快照隔离。
  • "multiple parallel chunks" >> 这是一个单一的进程,它从不同的线程产生多个任务来执行此操作,希望加快进程?您不会从这种工作方式中获得任何好处。最好从一个“脚本”或“过程”中执行此操作,并让 SQL Server 在它认为合适的情况下进行并行化。
  • 如果我们可以这样称呼它,您应该制定“算法”,在您的问题中一目了然。在什么线程上发生了什么,控制流和事件顺序是什么......从你写的内容来看有点不清楚。 SELECT 的结果是什么?一个身份证;还是很多ID?如果您回答多个 ID,您会做什么:每个 ID 的事务或所有 ID 的事务。是SELECT一个块的结果,还是你有很多选择,每个选择一个块。 SELECT 是事务的一部分吗?为什么在一个事务中只更新一个 ID 而删除其他几个 ID?等等。

标签: sql sql-server sql-server-2014 deadlock


【解决方案1】:

关于死锁的一般建议:确保以相同的顺序执行所有操作,即为不同的进程以相同的顺序获取锁。

您可以在 microsoft.com 上的这篇关于 Minimizing Deadlocks 的技术文章中找到相同的建议。将它排在第一位是有充分理由的。

  • 以相同的顺序访问对象。
  • 避免交易中的用户交互。
  • 保持交易简短,并在一批。
  • 使用较低的隔离级别。
  • 使用基于行版本控制的隔离级别。
  • 将 READ_COMMITTED_SNAPSHOT 数据库选项设置为 ON 以启用已提交读事务以使用行版本控制。
  • 使用快照隔离。
  • 使用绑定连接。

Cato 提问后更新:

在这里如何以相同的顺序获取锁?您对他将如何更改他的 SQL 来做到这一点有任何建议吗?

无论在什么环境下,死锁总是相同的:两个进程(比如AB)以不同的顺序获取多个锁(比如XY),因此A 正在等待对于YB 正在等待X,而A 正在等待X 并且B 正在等待Y

这里适用,因为DELETEUPDATE 语句隐含地获取行或索引范围或表上的锁(取决于引擎认为合适的内容)。

您应该分析您的流程,看看是否存在可以以不同顺序获取锁的情况。如果这没有透露任何信息,您可以analyze deadlocks using the SQL Server Profiler

要跟踪死锁事件,请将死锁图事件类添加到跟踪中。此事件类使用有关死锁中涉及的进程和对象的 XML 数据填充跟踪中的 TextData 数据列。 SQL Server Profiler 可以将 XML 文档提取到死锁 XML (.xdl) 文件中,稍后您可以在 SQL Server Management Studio 中查看该文件。您可以将 SQL Server Profiler 配置为将死锁图事件提取到包含所有死锁图事件的单个文件中,或提取到单独的文件中。

【讨论】:

  • 这里如何以相同的顺序获取锁?你对他如何改变他的 SQL 来做到这一点有什么建议吗?
  • 在问题中添加了死锁图,它们似乎都只指delete语句
【解决方案2】:

我会在更新事务中使用sp_getapplock 来防止此代码的多个实例并行运行。这不会像表锁定提示那样阻塞选择语句。

您仍然应该编写重试逻辑,因为获取锁可能需要一段时间,比超时参数更长。

这就是将更新事务包装到sp_getapplock 中的方式。

BEGIN TRANSACTION;
BEGIN TRY

    DECLARE @VarLockResult int;
    EXEC @VarLockResult = sp_getapplock
        @Resource = 'some_unique_name_app_lock',
        @LockMode = 'Exclusive',
        @LockOwner = 'Transaction',
        @LockTimeout = 60000,
        @DbPrincipal = 'public';

    IF @VarLockResult >= 0
    BEGIN
        -- Acquired the lock
        update FOO set ...
        where NON_CLUSTERED_ID = @id

        delete FOO where NON_CLUSTERED_ID in (@id1, @id2, @id3, ...)

    END ELSE BEGIN
        -- return some error code, so that the caller could retry
    END;

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION;
    -- handle the error
END CATCH;

选择语句不需要任何更改。

我建议不要使用NOLOCK,即使您说块中的 ID 不重叠。有了这个提示,SELECT 查询可以跳过一些正在更改的页面,它可以读取一些页面两次。这种行为是不可能被容忍的。

【讨论】:

  • 看起来sp_getapplock 会比我当前的实现慢,我最好重试所有死锁。感谢NOLOCK 上的信息。
  • 虽然这肯定是大锤方法,但我可以想象在某些情况或体系结构中,这是唯一可行的方法,而不是重新设计或重构程序。虽然总的来说,我建议尝试找到解决任何死锁情况的根本原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多