2011-04-06 81 views
8

我正在测试一次删除许多记录的进程。它不能TRUNCATE TABLE,因为那里有记录需要保留。超过SQL Server锁定超时删除循环中的记录

因为体积的,我已经打破了删除成类似于这样一个循环:

-- 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

- 编辑 -

我加了锁:超时事件探查器。它在删除超时上的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上获取锁?

+0

你试过跟踪SQL跟踪的各种锁定事件,看看你能不能拆洗发生了什么? – 2011-04-06 22:13:46

+0

刚刚做了,谢谢你提到这一点。我在上面添加了锁定超时信息。不确定究竟是什么被锁定。 – 2011-04-06 22:41:56

+0

另一个编辑:在锁定超时后立即添加一些sp_lock信息。 – 2011-04-06 22:58:57

回答

6

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

使用尼古拉斯的建议,我添加了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的内部进程就会运行,以回收已完全删除的记录页面(即所有记录都是幻影记录)。

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毫秒的锁定等待超时。这会导致鬼记录清理过程中偶尔发生锁定等待超时,但这是可以接受的。我还添加了一个循环,将锁定超时重试次数高达5次。有了这两个变化,我现在的过程通常会完成。现在,如果有一个非常长的进程推动大量数据获取表或页面锁定数据,我的进程需要清理,它只会得到超时。

编辑2016年7月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) 
+0

您可以在修复后发布您的生产解决方案吗? – 2016-07-19 15:48:48

+0

@RonnieOverby我添加了一个示例解决方案。我们的实际生产代码比这更复杂,因为它通过动态SQL清理了几个不同的表。上面的代码不包括额外的行李。 – 2016-07-20 14:05:30

+0

太棒了。感谢您花时间做到这一点。 – 2016-07-20 20:13:23

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 

这表明你的意图,并确保当他们应该是释放锁。

+1

SQL不会将行锁升级为页锁 - 它直接升级为表锁。 http://www.sqlskills.com/BLOGS/PAUL/post/A-SQL-Server-DBA-myth-a-day-(2330)-lock-escalation.aspx – 2011-04-06 21:39:04

+0

你是正确的,代码设置LOCK_TIMEOUT为0我只是把上面的内容包括在内;对不起,先不提。 – 2011-04-06 22:08:54

+0

在事务中封装这个功能有助于在锁定超时时识别打开的锁。请参阅上面的编辑。 – 2011-04-06 22:59:55