【问题标题】:TSQL deadlock on select with XLOCK ROWLOCK使用 XLOCK ROWLOCK 选择时出现 TSQL 死锁
【发布时间】:2017-09-07 08:33:14
【问题描述】:

简单的问题 - 我在 SQL Server 表中有 300k 个任务,我希望多个进程一个一个地选择它们,处理它们并保存结果。

我不时在挑选和保存方面陷入僵局。

我需要确保两个进程不会选择相同的任务。所以我使用XLOCK,在完成任务后,我将状态从 1-Created 更改为 2-Started,并在处理后更改为 3-Completed。

我的任务 (tblTasksSets) 也被一对一地引用到 tblGeneralSets(请不要问;)),tblGeneralSets 被多对一地引用到 tblContainers

所以我有两个程序来更新和选择挑选的任务:

DECLARE @SetIds AS TABLE(Id INT)    
-- Updating status
;WITH innerTable  AS 
(
    SELECT TOP 1 taskSets.* 
    FROM tblTasksSets taskSets WITH (XLOCK ROWLOCK) 
    INNER JOIN tblGeneralSets generalSets WITH(XLOCK ROWLOCK) ON generalSets.TaskSetId = taskSets.Id  
    WHERE generalSets.ContainerId = @ContainerId
      AND taskSets.ParameterSetStatusId = 1 --CREATED
) 
UPDATE innerTable 
SET ParameterSetStatusId = 2 -- STARTED
OUTPUT INSERTED.Id INTO @SetIds

-- Here are some unrelated updates on log history tables

-- And returning result
SELECT
    taskSets.*, containers.*
FROM 
    tblTasksSets taskSets
INNER JOIN 
    tblGeneralSets generalSets ON generalSets.TaskSetId = taskSets.Id
INNER JOIN 
    tblContainers containers ON containers.Id = generalSets.ContainerId
WHERE 
    taskSets.Id = (SELECT TOP 1 Id FROM @SetIds)

第二个将任务标记为已完成:

UPDATE tblTasksSets 
SET 
....
WHERE Id = @Id

任何想法为什么我会陷入僵局?我只希望一个进程等待另一个进程完成更新。

System.Data.SqlClient.SqlException (0x80131904):事务(进程 ID 53)与另一个进程在锁资源上死锁,并已被选为死锁牺牲品。重新运行事务。

在测试时,此数据库上没有运行其他查询。每隔几秒可能只有 50 个进程调用这两个存储过程

sp_lock shows something like this

Deadlock Graph

【问题讨论】:

  • 我很确定,当您在此期间运行sp_lock 时,您会看到一个连接在它只想阻塞一个时阻塞了很多行。这将是由于连接和/或缺乏适当的索引。即使不是这种情况,其他连接仍然需要等待以确保当前锁定的行不满足它们的where,因此您的并发性为零。
  • 您可能需要update top (1) ... from ... with (rowlock, holdlock, readpast) where ... The readpast hint.
  • 你能发布死锁图吗? sqlmag.com/database-performance-tuning/…
  • GSerg 我在做一些小研究,发现在某些情况下,在这种情况下可能会发生两个查询会选择一个项目,显然更新不是原子操作
  • 您是否使用事务,您的隔离级别是多少?

标签: sql sql-server tsql deadlock database-deadlocks


【解决方案1】:

有一种暴力方式可以序列化对资源的访问。使用 sql server 的内置锁定,您可以使用SP_GETAPPLOCK() 对您的工作进行排队访问。一个例子是:

CREATE PROC MyCriticalWork(@MyParam INT)      
AS
    DECLARE @LockRequestResult INT = 0    
    DECLARE @MyTimeoutMiliseconds INT =5000--Wait only five seconds max then timeout

    BEGIN TRAN

    EXEC @LockRequestResult=SP_GETAPPLOCK 'MyCriticalWork','Exclusive','Transaction',@MyTimeoutMiliseconds
    IF(@LockRequestResult>=0)BEGIN

            /*
            DO YOUR CRITICAL READS AND WRITES HERE
            */

        COMMIT TRAN -- <--Releases the lock!
    END ELSE
        ROLLBACK TRAN

【讨论】:

  • 罗斯布什谢谢我不知道那个功能。我仍在研究人们对我的问题发表评论时提供的其他选项,但可以肯定的是,我会使用你的方法作为我最后的手段。有人给那个人一些分数;)我不能因为是 1 小时老用户。
  • 这会序列化所有调用并确保没有并发。我相信 OP 的想法是不同的连接可以同时工作。
  • 我的主要目标是确保两个连接不会选择同一个任务来处理。 Ross 提供的解决方案实际上确保了这一点,但缺点是我需要在我更新关键表的所有地方实现它。我宁愿使用基于表格提示的解决方案,但如果找不到,我将使用 Ross 提供的解决方案。
  • 您可能会在dba.stackexchange.com 获得更好的答案。一般的经验法则是,死锁牺牲品 sql server 选择拥有最少资源的事务,并假设它最容易回滚。如果在死锁争用中存在读取和写入,它将“几乎”始终是回滚的读取。您可以通过使用 nolock 进行提示来减轻在高事务环境中的可怕情况下重试事务的情况,但是,您最好确保在这种情况下您的读取查询返回脏数据是可以的。
  • @devops_granted The readpast hint 专门用于支持要声明的不同连接的作业项队列。您开始事务,通过使用updlock, rowlock, readpast 选择它来锁定一条记录,您完成工作,提交释放锁定的事务。您不需要要更新的 status 字段,并且设置该字段通常是无用的,因为无论如何都无法读取记录。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-12-10
  • 2015-04-05
  • 1970-01-01
  • 1970-01-01
  • 2020-06-07
  • 2023-04-09
  • 1970-01-01
相关资源
最近更新 更多