SQL Server 循环删除记录超出锁定超时

标签 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 block 中运行了 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 上的锁?

最佳答案

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

根据 Nicholas 的建议,我添加了一个 BEGIN TRANSACTION 和一个 COMMIT。我将删除循环包装在 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 删除记录时,它会在记录上设置一个位,将它们标记为“幽灵记录”。每隔几分钟,就会运行一个名为“幽灵清理”的内部进程来回收已完全删除的记录页面(即所有记录都是幽灵记录)。

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)

关于SQL Server 循环删除记录超出锁定超时,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5572585/

相关文章:

sql - 删除一条sql语句中的所有表、存储过程、触发器、约束和所有依赖项

sql-server - CHECKSUM() 和 BINARY_CHECKSUM() 之间有什么区别以及何时/什么是合适的使用场景?

SQL 行值作为列名

sql-server - SQL 插入选择@@Identity

sql - 为什么我无法使用此动态 SQL 创建表?

sql-server - 如何在 SQL Server 中查找当前长时间运行的查询以及如何立即终止它们?

sql-server-2005 - 如何从SQL Server获取DOMAIN名称?

mysql - 单表之间一对一的SQL

mysql - SQL 获取非规范化表中某指定信息对应的第一个字段

php - 使用php从mysql数据库中选择特定内容