【问题标题】:SQL Server race condition issue with range lock范围锁定的 SQL Server 竞争条件问题
【发布时间】:2012-07-05 21:12:57
【问题描述】:

我正在 SQL Server 中实现一个队列(请不要对此进行讨论)并且遇到了竞争条件问题。感兴趣的 T-SQL 如下:

set transaction isolation level serializable
begin tran
declare @RecordId int
declare @CurrentTS datetime2
set @CurrentTS=CURRENT_TIMESTAMP
select top 1 @RecordId=Id from QueuedImportJobs with (updlock) where Status=@Status and (LeaseTimeout is null or @CurrentTS>LeaseTimeout) order by Id asc
if @@ROWCOUNT> 0
begin
update QueuedImportJobs set LeaseTimeout = DATEADD(mi,5,@CurrentTS), LeaseTicket=newid() where Id=@RecordId
select * from QueuedImportJobs where Id = @RecordId
end
commit tran

RecordId 是 PK,Status,LeaseTimeout 上也有索引。

我所做的基本上是选择一条租约恰好到期的记录,同时将租约时间更新为 5 分钟并设置新的租约票证。

所以问题是,当我使用几个线程并行运行此代码时,我遇到了死锁。我已经对其进行了调试,直到发现更新语句有时会为同一条记录执行两次。现在,我的印象是with (updlock) 应该防止这种情况发生(顺便说一句,xlock 也会发生这种情况,而不是tablockx)。所以看起来实际上在同一范围的记录上有一个 RangeS-U 和一个 RangeX-X 锁,这应该是不可能的。

那么我错过了什么?我认为这可能与前 1 子句有关,或者 SQL Server 不知道 where Id=@RecordId 实际上在锁定范围内?

死锁图:

表架构(简化):

【问题讨论】:

    标签: sql-server tsql locking deadlock


    【解决方案1】:

    看起来锁在不同的 HOBT 上。表上有多个索引吗?

    如果是这样,则选择 with (updlock) 可能只会在一个索引上使用 update 锁定。

    【讨论】:

    • 是的,有 2 个索引。你能暗示使用这两个索引吗?我对HOBT不太熟悉,所以也许你能启发我:)
    • hobt 是没有索引的表(堆)或二叉树(索引)。您不能暗示锁定多个索引。 AaronBertrand 的回答应该会有所帮助,我们使用类似的东西。
    【解决方案2】:

    为什么不只是:

    DECLARE @t TABLE(Id INT);
    
    UPDATE TOP (1) dbo.QueuedImportJobs 
      SET LeaseTimeout = DATEADD(MINUTE, 5, CURRENT_TIMESTAMP)
      OUTPUT inserted.Id INTO @t
      WHERE Status = @Status 
      AND COALESCE(LeaseTimeout, '19000101') < CURRENT_TIMESTAMP;
    
    SELECT <cols> FROM dbo.QueuedImportJobs 
      WHERE Id IN (SELECT Id FROM @t);
    

    顺便说一句,您可能需要ORDER BY 以确保根据所需的索引顺序,所选行是队列中的第一个行。如果 Id 上的索引是聚集的,这可能就是它无论如何都会工作的方式,但除非你这么说,否则无法保证。这将需要对查询进行轻微的重组,因为您不能直接在 UPDATE 上应用 ORDER BY(或索引提示),例如:

    WITH x AS
    (
      SELECT TOP (1) Id, LeaseTimeout
        FROM dbo.QueuedImportJobs
        WHERE Status = @Status 
        AND COALESCE(LeaseTimeout, '19000101') < CURRENT_TIMESTAMP
        ORDER BY Id
    )
    UPDATE x
      SET LeaseTimeout = DATEADD(MINUTE, 5, CURRENT_TIMESTAMP)
      OUTPUT inserted.id INTO @t;
    

    【讨论】:

    • 使用此查询,OP 将如何检索声明的作业的详细信息?输出子句可能会有所帮助
    • 这可能真的有效,我明天去看看。仍然想知道为什么我的解决方案不起作用。
    • @Freek 我认为 Andomar 是对的,您在两个不同的索引上陷入僵局,考虑到您提供了架构的简化版本,这可能并不明显。但过去我通过将客户的SELECT ... if rows then do stuff ... UPDATE/DELETE 简化为更简单、直接、单一的原子操作,为客户解决了许多死锁问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-30
    • 1970-01-01
    • 2010-10-30
    • 1970-01-01
    • 2011-09-16
    相关资源
    最近更新 更多