【问题标题】:SQL Server Lock Timeout Exceeded Deleting Records in a LoopSQL Server 锁定超时超出循环删除记录
【发布时间】:2011-04-06 20:33:03
【问题描述】:

我正在测试一个同时删除很多很多记录的过程。不能TRUNCATE TABLE,因为里面有记录需要保留。

因为体积的原因,我把delete分成了类似这样的循环:

-- Do not block if records are locked.
SET LOCK_TIMEOUT 0
-- This process should be chosen as a deadlock victim in the case of a deadlock.
SET DEADLOCK_PRIORITY LOW
SET NOCOUNT ON

DECLARE @Count
SET @Count = 1
WHILE @Count > 0
BEGIN TRY
    BEGIN TRANSACTION -- added per comment below

    DELETE TOP (1000) FROM MyTable WITH (ROWLOCK, READPAST) WHERE MyField = SomeValue
    SET @Count == @@ROWCOUNT

    COMMIT
END TRY
BEGIN CATCH
    exec sp_lock -- added to display the open locks after the timeout
    exec sp_who2 -- shows the active processes

    IF @@TRANCOUNT > 0
        ROLLBACK
    RETURN -- ignoring this error for brevity
END CATCH

MyTable 是一个聚簇表。 MyField 位于聚集索引的第一列。它表示记录的逻辑分组,因此MyField = SomeValue 经常选择许多记录。只要一次处理一组,我不在乎它们被删除的顺序。此表上没有其他索引。

我添加了ROWLOCK 提示,以尽量避免我们在生产中看到的锁升级。我添加了READPAST 提示以避免删除被其他进程锁定的记录。那不应该发生,但我正在努力确保安全。

问题:当它是唯一运行的东西时,有时这个循环会遇到锁定超时 1222“超过锁定请求超时期限”。

我确信在我测试这个进程时这个系统上没有其他活动,因为它是我自己的开发者盒子,没有其他人连接,没有其他进程在上面运行,并且分析器显示没有活动。

我可以在一秒钟后重新运行相同的脚本,它会从中断处继续,愉快地删除记录——直到下一次锁定超时。

我已尝试使用BEGIN TRY / BEGIN CATCH 忽略 1222 错误并重试删除,但它立即再次失败并出现相同的锁定超时错误。如果我在重试之前添加一个短暂的延迟,它也会再次失败。

我认为锁定超时是由于页面拆分之类的原因,但我不确定为什么这会与当前循环迭代冲突。之前的 delete 语句应该已经完成​​了,我认为这意味着任何页面拆分也完成了。

为什么 DELETE 循环会针对自身遇到锁定超时?

进程是否有办法避免此锁定超时或检测到可以安全恢复?

这是在 SQL Server 2005 上。

-- 编辑--

我将 Lock:Timeout 事件添加到分析器。删除期间 PAGELOCK 超时:

Event Class: Lock:Timeout
TextData:    1:15634  (one example of several)
Mode:        7 - IU
Type:        6 - PAGE

DBCC PAGE 报告这些页面超出了主数据库 (ID 1) 的范围。

-- 编辑 2--

我添加了 BEGIN TRY / BEGIN CATCH 并在 catch 块中运行了 exec sp_lock。这是我看到的:

spid dbid ObjId      IndId Type Resource Mode Status
19   2    1401108082 1     PAG  1:52841  X    GRANT  (tempdb.dbo.MyTable)
19   2    1401108082 0     TAB           IX   GRANT  (tempdb.dbo.MyTable)
Me   2    1401108082 0     TAB           IX   GRANT  (tempdb.dbo.MyTable)
Me   1    1115151018 0     TAB           IS   GRANT  (master..spt_values)  (?)

SPID 19 是一个 SQL Server 任务管理器。为什么这些任务管理器之一会获取 MyTable 上的锁?

【问题讨论】:

  • 您是否尝试过跟踪 SQL Trace 中的各种锁定事件以查看是否可以取消选择正在发生的事情?
  • 刚刚做了,谢谢你提到这一点。我在上面添加了锁定超时信息。不确定究竟是什么被锁定。
  • 另一个编辑:在锁定超时后立即添加了一些 sp_lock 信息。

标签: sql sql-server sql-server-2005 locking


【解决方案1】:

我找到了答案:我的循环删除与幽灵清理过程冲突。

根据 Nicholas 的建议,我添加了 BEGIN TRANSACTIONCOMMIT。我将删除循环包裹在 BEGIN TRY / BEGIN CATCH 中。在BEGIN CATCH 中,就在ROLLBACK 之前,我运行了sp_locksp_who2。 (我在上面的问题中添加了代码更改。)

当我的进程被阻塞时,我看到了以下输出:

spid   dbid   ObjId       IndId  Type Resource                         Mode     Status
------ ------ ----------- ------ ---- -------------------------------- -------- ------
20     2      1401108082  0      TAB                                   IX       GRANT
20     2      1401108082  1      PAG  1:102368                         X        GRANT

SPID  Status     Login HostName BlkBy DBName Command       CPUTime DiskIO
----  ---------- ----- -------- ----- ------ ------------- ------- ------
20    BACKGROUND sa    .        .     tempdb GHOST CLEANUP 31      0

为了将来参考,当 SQL Server 删除记录时,它会在它们上设置一个位,将它们标记为“幽灵记录”。每隔几分钟,就会运行一个名为 ghost cleanup 的内部进程,以回收已完全删除的记录页面(即所有记录都是 ghost 记录)。

The ghost cleanup process was discussed on ServerFault in this question.

Here is Paul S. Randal's explanation of the ghost cleanup process.

It is possible to disable the ghost cleanup process with a trace flag. 但在这种情况下我不必这样做。

我最终添加了 100 毫秒的锁定等待超时。这会导致幽灵记录清理过程中偶尔出现锁定等待超时,但这是可以接受的。我还添加了一个 our 循环,该循环重试锁定超时最多 5 次。通过这两个更改,我的流程现在通常完成。现在,只有当有一个非常长的进程推送大量数据并在我的进程需要清理的数据上获取表或页锁时,它才会超时。

编辑 2016-07-20

最终代码如下所示:

-- Do not block long if records are locked.
SET LOCK_TIMEOUT 100

-- This process volunteers to be a deadlock victim in the case of a deadlock.
SET DEADLOCK_PRIORITY LOW

DECLARE @Error BIT
SET @Error = 0

DECLARE @ErrMsg VARCHAR(1000)
DECLARE @DeletedCount INT
SELECT @DeletedCount = 0

DECLARE @LockTimeoutCount INT
SET @LockTimeoutCount = 0

DECLARE @ContinueDeleting BIT,
    @LastDeleteSuccessful BIT

SET @ContinueDeleting = 1
SET @LastDeleteSuccessful = 1

WHILE @ContinueDeleting = 1
BEGIN
    DECLARE @RowCount INT
    SET @RowCount = 0

    BEGIN TRY

        BEGIN TRANSACTION

        -- The READPAST below attempts to skip over locked records.
        -- However, it might still cause a lock wait error (1222) if a page or index is locked, because the delete has to modify indexes.
        -- The threshold for row lock escalation to table locks is around 5,000 records,
        -- so keep the deleted number smaller than this limit in case we are deleting a large chunk of data.
        -- Table name, field, and value are all set dynamically in the actual script.
        SET @SQL = N'DELETE TOP (1000) MyTable WITH(ROWLOCK, READPAST) WHERE MyField = SomeValue' 
        EXEC sp_executesql @SQL, N'@ProcGuid uniqueidentifier', @ProcGUID

        SET @RowCount = @@ROWCOUNT

        COMMIT

        SET @LastDeleteSuccessful = 1

        SET @DeletedCount = @DeletedCount + @RowCount
        IF @RowCount = 0
        BEGIN
            SET @ContinueDeleting = 0
        END

    END TRY
    BEGIN CATCH

        IF @@TRANCOUNT > 0
            ROLLBACK

        IF Error_Number() = 1222 -- Lock timeout
        BEGIN

            IF @LastDeleteSuccessful = 1
            BEGIN
                -- If we hit a lock timeout, and we had already deleted something successfully, try again.
                SET @LastDeleteSuccessful = 0
            END
            ELSE
            BEGIN
                -- The last delete failed, too.  Give up for now.  The job will run again shortly.
                SET @ContinueDeleting = 0
            END
        END
        ELSE -- On anything other than a lock timeout, report an error.
        BEGIN       
            SET @ErrMsg = 'An error occurred cleaning up data.  Table: MyTable Column: MyColumn Value: SomeValue.  Message: ' + ERROR_MESSAGE() + ' Error Number: ' + CONVERT(VARCHAR(20), ERROR_NUMBER()) + ' Line: ' + CONVERT(VARCHAR(20), ERROR_LINE())
            PRINT @ErrMsg -- this error message will be included in the SQL Server job history
            SET @Error = 1
            SET @ContinueDeleting = 0
        END

    END CATCH

END

IF @Error <> 0
    RAISERROR('Not all data could be cleaned up.  See previous messages.', 16, 1)

【讨论】:

  • 您能在修复后发布您的生产解决方案吗?
  • @RonnieOverby 我添加了一个示例解决方案。我们的实际生产代码比这更复杂,因为它通过动态 SQL 清理了几个不同的表。上面的代码不包括额外的行李。
  • 太棒了。感谢您花时间这样做。
【解决方案2】:

您或使用该连接的其他人正在将锁定超时设置为默认值以外的值。详情请见http://msdn.microsoft.com/en-US/library/ms189470(v=SQL.90).aspx

默认锁定时间是-1毫秒,意思是“永远等待”。

行提示很好,但它们是代码味道,应该避免。让 SQL Server 完成它的工作。它比您获得的关于整个系统的信息更多。

对于初学者,您无法控制锁大小:锁升级会根据未完成锁的数量自动发生。它从行锁开始。如果累积太多行锁,SQL Server 会升级为页锁。获取过多的页锁并升级为表锁。有关锁定升级的详细信息,请参阅 http://msdn.microsoft.com/en-us/library/ms184286(v=SQL.90).aspx。但是,您可以设置几个跟踪标志来防止锁升级:但是,这会降低 SQL Server 的性能。

另一件事:您应该将DELETE 语句包装在事务中,尤其是在存储过程中。

DECLARE @Count INT
SET @Count = 1
WHILE @Count > 0
  BEGIN
    BEGIN TRANSACTION
    DELETE TOP (1000) FROM MyTable WITH (ROWLOCK, READPAST) WHERE MyField = SomeValue
    SET @Count = @@ROWCOUNT
    COMMIT TRANSACTION
  END

这清楚地表明您的意图并确保在应该释放锁时释放锁。

【讨论】:

  • SQL 不会将行锁升级为页锁——它会直接升级为表锁。 sqlskills.com/BLOGS/PAUL/post/…
  • 您是正确的,代码将 LOCK_TIMEOUT 设置为 0。我只是在上面包含了它;很抱歉没有早点提及。
  • 将其包装在事务中有助于在锁定超时时识别打开的锁定。请参阅上面的修改。
猜你喜欢
  • 1970-01-01
  • 2010-09-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-05
  • 1970-01-01
  • 2023-04-11
  • 2010-12-24
相关资源
最近更新 更多