Search code examples
sqlt-sqlsql-server-2008-r2database-deadlocks

Deadlock caused by retried INSERTs


The following sproc attempts to insert a row into a table and generate a random ID, which is used for a PK on the corresponding table. Collisions with randomly generated IDs are handled in the catch block, where the procedure is retried/called again. Now, this takes a long time and causes deadlocks, because locks are kept for an extended period of time. Is there a way to release deadlocks immediately before retrying, so that there is a short window when other threads can succeed in locking the PK index?


CREATE PROCEDURE addPerson
    (
        @FirstName nvarchar(100),
        @LastName nvarchar(100)
    )
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @PersonId int

    -- generate random PersonId
    -- this sproc can generate ids that already exist in the table
    EXEC generateRandomPersonId @PersonId=@PersonId OUTPUT

    BEGIN TRY       
        INSERT INTO [dbo].[Persons] 
        (
            PersonId,FirstName,LastName
        )
        VALUES 
        (
            @PersonId,@FirstName,@LastName
        )

    BEGIN CATCH
        -- 
        -- HOW TO RELEASE LOCKS HERE that are still held
        -- for the previous INSERT statement?
        --

        DECLARE @ErrorNumber int, @ErrorMessage nvarchar(2048)
        SELECT  @ErrorNumber=ERROR_NUMBER(), 
                @ErrorMessage=ERROR_MESSAGE()

        -- if a race condition happened and 
        -- PersonId happened to be picked already, retry all over again
        IF (@ErrorNumber = 2601 OR @ErrorNumber = 2627 AND CHARINDEX(N'PK_Persons_PersonId', @ErrorMessage) > 0)
            BEGIN
               --
               -- RETRYING HERE participates in a high possibility and 
               -- occurrence of deadlocks
               -- 
               EXEC addPerson @FirstName,@LastName
            END
            ELSE 
               -- some other error, rethrow it
               EXEC rethrowError
            END
    END CATCH
END
GO

Solution

  • A process does not block on its own locks. Since a call to a stored procedure runs in the same process, it's impossible for the second insert to wait for a lock from the first insert.

    Could you post a deadlock graph? That shows a lot of information on the blocking processes.

    As a quick fix, you could search for a free ID in a loop, which would avoid most (but not all) possible clashes:

    while 1=1
        begin
        EXEC generateRandomPersonId @PersonId=@PersonId OUTPUT
        if not exists (select * from Persons where PersonId = @PersonID)
            break
        end