Превышено время ожидания блокировки SQL Server. Удаление записей в цикле - PullRequest
8 голосов
/ 07 апреля 2011

Я тестирую процесс, который удаляет много, много записей одновременно. Он не может 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 «Превышен период ожидания запроса блокировки», когда он работает только *. 1015 *

Я уверен, что в этой системе нет других действий, пока я тестирую этот процесс, потому что это мой собственный ящик разработчика, никто не подключен, другие процессы не выполняются, и профилировщик не показывает активности.

Я могу повторно запустить тот же сценарий через секунду, и он начинает с того места, на котором остановился, удачно удаляя записи - до следующего таймаута блокировки.

Я попытался 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 и набрал exec sp_lock в блоке catch. Вот что я увидел:

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 ]

6 голосов
/ 08 апреля 2011

Я нашел ответ: мое зацикленное удаление конфликтует с процедурой очистки ghost.

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

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

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

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

Я добавил время ожидания блокировки 100 мс. Это приводит к случайным тайм-аутам ожидания блокировки в процессе очистки записи-призрака, но это приемлемо. Я также добавил наш цикл, который повторяет время ожидания блокировки до 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)
4 голосов
/ 07 апреля 2011

Вы или кто-либо еще, использующий соединение, устанавливаете тайм-аут блокировки, отличный от значения по умолчанию.Подробнее см. 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

Это проясняет ваше намерение и обеспечивает освобождение блокировок, когда они должны быть.

...