Время ожидания блокировки SQL Server превысило время удаления записей в цикле

я тестирую процесс, который удаляет много-много записей. Не может!--3-->, потому что там есть записи, которые должны остаться.

из-за громкости я разбил 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 должна была быть уже завершена, и я подумал, что это означает, что любые разбиения страниц также завершены.

почему цикл удаления поражает таймаут блокировки против себя?

есть ли способ, процесс может избежать этого тайм-аута блокировки или обнаружить, что это безопасно возобновить?

это на SQL Server 2005.

-- EDIT --

я добавил событие Lock:Timeout в профилировщик. Это тайм-аут на PAGELOCK во время удаления:

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

DBCC PAGE reports эти страницы находятся вне диапазона главной базы данных (ID 1).

-- EDIT 2 --

добавил BEGIN TRY / BEGIN CATCH и exec sp_lock в блоке catch. Вот что я увидел:--13-->

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?

2 ответов


я нашел ответ: мое циклическое удаление противоречит proc очистки призрака.

используя предложение Николаса, я добавил 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 удаляет записи, он устанавливает немного на них, чтобы просто пометить их как "записи-призраки". Каждые несколько минут выполняется внутренний процесс под названием ghost cleanup для восстановления страниц записей, которые были полностью удалены (т. е. все записи являются записями-призраками).

процесс очистки призрака обсуждался на ServerFault в этом вопросе.

вот объяснение пола С. Рэндала процесса очистки призраков.

можно отключите процесс очистки призрака с помощью флага трассировки. но в этом случае мне не нужно было этого делать.

я закончил тем, что добавил тайм-аут ожидания блокировки 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)

Вы или кто-то другой, используя соединение настройки таймаута блокировки на что-то другое, чем по умолчанию. См.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(в=в SQL.90).аспн на укрупнение детали. Однако есть несколько флагов трассировки, которые вы можете установить, что предотвратит эскалация блокировки: однако это приведет к снижению производительности 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

это делает ясным ваше намерение и гарантирует, что замки освобождаются, когда они должны быть.