Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Tempo limite de bloqueio do SQL Server excedido excluindo registros em um loop


Eu encontrei a resposta:minha exclusão em loop está em conflito com o procedimento de limpeza fantasma.

Usando a sugestão de Nicholas, adicionei um BEGIN TRANSACTION e um COMMIT . Envolvi o loop de exclusão em um BEGIN TRY / BEGIN CATCH . No BEGIN CATCH , logo antes de um ROLLBACK , executei sp_lock e sp_who2 . (Adicionei as alterações de código na pergunta acima.)

Quando meu processo foi bloqueado, vi a seguinte saída:
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

Para referência futura, quando o SQL Server exclui registros, ele define um pouco neles para apenas marcá-los como "registros fantasmas". A cada poucos minutos, um processo interno chamado limpeza fantasma é executado para recuperar páginas de registros que foram totalmente excluídos (ou seja, todos os registros são registros fantasmas).

O processo de limpeza fantasma foi discutido no ServerFault nesta pergunta.

Aqui está Paul A explicação de S. Randal sobre o processo de limpeza de fantasmas.

É possível desabilitar o processo de limpeza fantasma com um sinalizador de rastreamento. Mas eu não tive que fazer isso neste caso.

Acabei adicionando um tempo limite de espera de bloqueio de 100 ms. Isso causa tempos limite de espera de bloqueio ocasionais no processo de limpeza de registro fantasma, mas isso é aceitável. Eu também adicionei um nosso loop que tenta bloquear tempos limite até 5 vezes. Com essas duas alterações, meu processo agora geralmente é concluído. Agora, ele só obtém um tempo limite se houver um processo muito longo empurrando muitos dados que adquirem bloqueios de tabela ou página nos dados que meu processo precisa limpar.

EDITAR 20-07-2016

O código final fica assim:
-- 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)