重试 INSERT 导致的死锁

Posted

技术标签:

【中文标题】重试 INSERT 导致的死锁【英文标题】:Deadlock caused by retried INSERTs 【发布时间】:2011-12-19 20:46:53 【问题描述】:

下面的 sproc 尝试在表中插入一行并生成一个随机 ID,该 ID 用于在相应表上进行 PK。具有随机生成的 ID 的冲突在 catch 块中处理,在该块中再次重试/调用该过程。现在,这需要很长时间并导致死锁,因为锁会保留很长时间。有没有办法在重试前立即解除死锁,以便其他线程可以成功锁定 PK 索引时有一个短暂的窗口?


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

【问题讨论】:

这是用于商业或生产环境的产品吗? 为什么会有多个错误代码用于违反唯一约束?完全不懂SQL server,只是好奇 为什么不使用transactions?在try 之前开始事务并回滚您写comment_ HOW TO RELEASE ... . 而且不需要递归,你可以重写程序并使用循环。请记住,递归会增加堆栈。 为什么不检查生成的 id 是否已经存在于表中之前尝试做和INSERT 【参考方案1】:

进程不会阻塞自己的锁。由于对存储过程的调用在同一个进程中运行,因此第二个insert 不可能等待来自第一个insert 的锁。

你能发布一个死锁图吗?这显示了很多关于阻塞进程的信息。

作为一种快速解决方法,您可以在循环中搜索空闲 ID,这将避免大多数(但不是全部)可能的冲突:

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

【讨论】:

+1 - 这绝对是操作应该做的,搜索一个免费的 ID,然后做他的INSERT 它不会自己阻塞,而是与另一个需要获取 PK 索引锁定的过程一起阻塞。至于检查生成的id是否不存在,它不能保证唯一性,因为它不是线程安全的。 添加了一个检查以验证 id 没有被使用,并用循环替换递归调用,瞧!谢谢【参考方案2】:

首先,我希望这个程序只适用于学术环境,而不适用于商业产品或实际生产环境。

这是一种方法:

用循环替换递归 使用事务
CREATE PROCEDURE addPerson
    (
        @FirstName nvarchar(100),
        @LastName nvarchar(100)
    )
AS

BEGIN
  SET NOCOUNT ON;

  DECLARE @doed bit

  set @doed = 0

  DECLARE @PersonId int

  WHILE @doed = 0

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

    BEGIN TRANSACTION ExceptionHandling
    BEGIN TRY       

            INSERT INTO [dbo].[Persons] 
            (
                PersonId,FirstName,LastName
            )
            VALUES 
            (
                @PersonId,@FirstName,@LastName
            )
            COMMIT TRANSACTION ExceptionHandling
        BEGIN CATCH

            ROLLBACK TRANSACTION ExceptionHandling
            -- 
            -- 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
                   set @doed = 0
                END
                ELSE 
                   -- some other error, rethrow it
                   set @doed = 1
                   EXEC rethrowError
                END
        END CATCH
  END  --end while
 END
GO​

免责声明:未经测试

【讨论】:

以上是关于重试 INSERT 导致的死锁的主要内容,如果未能解决你的问题,请参考以下文章

两个INSERT发生死锁原因剖析

delete+insert 引发的死锁问题

insert …select …带来的死锁问题

死锁 BLOB INSERT MySQL 8.0 InnoDB 集群

INSERT 和 SELECT SP 之间的死锁

为啥并发的“删除...插入”语句会导致死锁?