SQL Server锁定超时超过删除循环中的记录

时间:2011-04-06 20:33:04

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

我正在测试一次删除许多记录的进程。它不能TRUNCATE TABLE,因为那里有记录需要留下来。

由于音量的原因,我将删除分解为类似于此的循环:

-- 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循环会对自己造成锁定超时?

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

这是在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 TASK MANAGER。为什么其中一个任务管理器会在MyTable上获取锁?

2 个答案:

答案 0 :(得分:6)

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

使用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记录”。每隔几分钟,就会运行一个名为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毫秒的锁等待超时。这会在ghost记录清理过程中偶尔导致锁定等待超时,但这是可以接受的。我还添加了一个循环,重试锁定超时最多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)

答案 1 :(得分:4)

您或其他使用该连接的人将锁定超时设置为默认值以外的值。有关详细信息,请参阅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

这样可以明确你的意图,并确保在应该发布时释放锁。