【问题标题】:How to prevent deadlock in concurrent T-SQL transactions?如何防止并发 T-SQL 事务中的死锁?
【发布时间】:2021-03-26 08:17:16
【问题描述】:

我有一个插入数百条记录的查询。查询背后的想法是:

  1. 删除旧记录id
  2. 插入具有相同id 的新记录
  3. 如果id 的记录不存在,将生成eternal_id 的值
  4. 如果id的记录存在,我们应该保存eternal_id的值
  5. 查询在事务中执行,Read Committed 类型

查询如下:

DECLARE @id1 int = 100
DECLARE @id2 int = 200

CREATE TABLE #t(
    [eternal_id] [uniqueidentifier] NULL,
    [id] [int] NOT NULL
)

DELETE FROM [dbo].[SomeTable] WITH (HOLDLOCK)
        OUTPUT
             DELETED.eternal_id
            ,DELETED.id
        INTO #t
WHERE [id] IN (@id1, @id2)

INSERT INTO [dbo].[SomeTable]
            ([id]
            ,[title]
            ,[eternal_id])
SELECT main.*, ISNULL([eternal_id], NEWID())
FROM
(

    SELECT
     @id1 Id
    ,'Some title 1' Title

    UNION

    SELECT
     @id2 Id
    ,'Some title 2' Title

) AS main
LEFT JOIN #t t ON main.[id] = t.[id]

DROP TABLE #t

我有数百个线程使用不同的@id 执行此查询。当[dbo].[SomeTable] 中的记录已经存在时,一切正常,但是当@id 的记录不存在时,我正在捕捉:

事务(进程 ID 73)在锁定资源上与另一个进程死锁,并已被选为死锁牺牲品。重新运行事务。

因此,当 2 个或更多并发线程通过相同的 @id 并且 [dbo].[SomeTable] 中不存在记录时,就会出现问题。 我试图在这里删除WITH (HOLDLOCK)

DELETE FROM [dbo].[SomeTable] WITH (HOLDLOCK)
        OUTPUT
             DELETED.eternal_id
            ,DELETED.id
        INTO #t
WHERE [id] IN (@id1, @id2)

这没用,我开始明白了:

违反主键约束“PK__SomeTable__3213E83F5D97F3D0”。无法在对象“dbo.SomeTable”中插入重复键。重复键值为 (49)。 声明已终止。

因此,如果没有 WITH (HOLDLOCK),即使记录已经存在,它也无法正常工作。

当表中不存在id的记录时如何防止死锁?

【问题讨论】:

  • 为什么要删除然后插入,而不是更新?更新不需要HOLDLOCK 提示,因为它是唯一有效的操作。
  • 1.我不明白为什么这不是更新 2. 不知道你为什么在这里使用临时表,我怀疑表变量所涉及的锁定会更少 3. WITH (HOLDLOCK, UPDLOCK) 有帮助吗? 4.请edit添加表的索引定义,死锁图并通过brentozar.com/pastetheplan分享查询计划
  • 虽然尝试避免所有死锁是一个雄心勃勃的目标……但这并不总是可能的……而且您无法阻止所有未来死锁的发生,因为随着更多行添加到表查询计划改变。任何应用程序代码都应该有某种形式的重试机制来处理这个问题。
  • @gvee 感谢您的回复。在幕后,更新是简单的deleteinsert 命令,所以仍然会发生死锁。更多@id,死锁的可能性更大。
  • @DaleK 感谢您的回复。我想我明白你在说什么。是的,我想我应该实现某种形式的重试机制来处理这个问题。

标签: sql-server multithreading tsql concurrency deadlock


【解决方案1】:

eternal_id 的条件更新可以这样完成:

update t set
    ...
    eternal_id = ISNULL(t.eternal_id, NEWID())
from [dbo].[SomeTable] t
where t.id = @id

因此,如果旧值存在,您将保留它。无需删除/插入。除非你在触发器中有一些魔法。

【讨论】:

  • 伊万,谢谢你的回复。在幕后,更新是简单的deleteinsert 命令,所以死锁仍然会发生。第二件事是我有一堆记录,但你的解决方案是 1 条记录。
  • your solution for 1 record 这是一个更新语法的例子。 WHEREJOIN 在这里的工作方式与其他语句中的方式相同。根据您的需要调整过滤器。
  • 是的,你是对的。这是有用的信息。但是对一堆记录的并发更新仍然会产生死锁,所以我决定实现某种形式的重试机制来处理这个问题。
  • 欢迎提出另一个指向死锁问题的问题,而不是一些代码细节。提供包括指标、样本数据集、死锁图在内的数据模型。详细说明如何从调用这些“更新”(线程)的一侧打开/关闭事务。目前我们正在讨论您的解决方案,而不是 XY 问题的任务/问题本身。死锁不是独角兽,不需要魔法来对抗它们。
【解决方案2】:

我认为@DaleK 的上述评论对我帮助最大。我会引用它:

虽然尝试避免所有僵局是一个雄心勃勃的目标......但它不是 总是可能的......而且你不能阻止所有未来的死锁 发生,因为随着更多行添加到表中,查询计划会发生变化。 任何应用程序代码都应该有某种形式的重试机制 处理这个(事情。 – 戴尔K

所以我决定实现某种形式的重试机制来处理这个问题。

【讨论】:

    猜你喜欢
    • 2016-06-02
    • 2020-07-08
    • 1970-01-01
    • 1970-01-01
    • 2017-03-27
    • 1970-01-01
    • 1970-01-01
    • 2018-08-04
    • 1970-01-01
    相关资源
    最近更新 更多