我正在测试一个可以一次删除很多很多记录的过程。它不能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_lock
和 sp_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/