【问题标题】:Insert Statement / Stored Proc dead locks插入语句/存储过程死锁
【发布时间】:2010-10-18 22:11:49
【问题描述】:

我有一个使用 linq 死锁的插入语句。所以我把它放在一个存储过程中,以防周围的语句影响它。

现在存储过程是死锁的。根据 Server Profiler,插入语句的某些内容正在锁定自身。它声称其中两个插入语句正在等待释放 PK 索引:

当我将代码放在存储过程中时,它现在声明此存储过程已与此存储过程的另一个实例死锁。

这里是代码。 select 语句类似于 linq 在执行自己的查询时使用的语句。我只是想看看该项目是否存在,如果不存在则插入它。我可以通过 PK 或一些查找值找到系统。

       SET NOCOUNT ON;
       BEGIN TRY
        SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

        BEGIN TRANSACTION SPFindContractMachine
        DECLARE @id int;
        set @id = (select [m].pkID from Machines as [m]
                        WHERE ([m].[fkContract] = @fkContract) AND ((
                        (CASE 
                            WHEN @bByID = 1 THEN 
                                (CASE 
                                    WHEN [m].[pkID] = @nMachineID THEN 1
                                    WHEN NOT ([m].[pkID] = @nMachineID) THEN 0
                                    ELSE NULL
                                 END)
                            ELSE 
                                (CASE 
                                    WHEN ([m].[iA_Metric] = @lA) AND ([m].[iB_Metric] = @lB) AND ([m].[iC_Metric] = @lC) THEN 1
                                    WHEN NOT (([m].[iA_Metric] = @lA) AND ([m].[iB_Metric] = @lB) AND ([m].[iC_Metric] = @lC)) THEN 0
                                    ELSE NULL
                                 END)
                         END)) = 1));
        if (@id IS NULL)
        begin
            Insert into Machines(fkContract, iA_Metric, iB_Metric, iC_Metric, dteFirstAdded) 
                values (@fkContract, @lA, @lB, @lC, GETDATE());

            set @id = SCOPE_IDENTITY();
        end

        COMMIT TRANSACTION SPFindContractMachine

        return @id;

    END TRY
    BEGIN CATCH
        if @@TRANCOUNT > 0
            ROLLBACK TRANSACTION SPFindContractMachine
    END CATCH

【问题讨论】:

    标签: c# linq-to-sql sql-server-2008 stored-procedures deadlock


    【解决方案1】:

    任何遵循模式的过程:

    BEGIN TRAN
    check if row exists with SELECT
    if row doesn't exist INSERT
    COMMIT
    

    在生产中会遇到麻烦,因为没有什么可以阻止两个胎面同时进行检查并且都得出应该插入的结论。特别是,在序列化隔离级别(如您的情况)下,此模式保证死锁。

    更好的模式是使用数据库唯一约束并始终插入,捕获重复的键违规错误。这也显着提高了性能。

    另一种选择是使用MERGE 语句:

    create procedure usp_getOrCreateByMachineID
        @nMachineId int output,
        @fkContract int,
        @lA int,
        @lB int,
        @lC int,
        @id int output
    as
    begin
        declare @idTable table (id int not null);
        merge Machines as target
            using (values (@nMachineID, @fkContract, @lA, @lB, @lC, GETDATE()))
                as source (MachineID, ContractID, lA, lB, lC, dteFirstAdded)
        on (source.MachineID = target.MachineID)
        when matched then
            update set @id = target.MachineID
        when not matched then
            insert (ContractID, iA_Metric, iB_Metric, iC_Metric, dteFirstAdded)
            values (source.contractID, source.lA, source.lB, source.lC, source.dteFirstAdded)
        output inserted.MachineID into @idTable;
        select @id = id from @idTable;
    end 
    go
    
    create procedure usp_getOrCreateByMetrics
        @nMachineId int output,
        @fkContract int,
        @lA int,
        @lB int,
        @lC int,
        @id int output
    as
    begin
        declare @idTable table (id int not null);
        merge Machines as target
            using (values (@nMachineID, @fkContract, @lA, @lB, @lC, GETDATE()))
                as source (MachineID, ContractID, lA, lB, lC, dteFirstAdded)
        on (target.iA_Metric = source.lA
            and target.iB_Metric = source.lB
            and target.iC_Metric = source.lC)
        when matched then
            update set @id = target.MachineID
        when not matched then
            insert (ContractID, iA_Metric, iB_Metric, iC_Metric, dteFirstAdded)
            values (source.contractID, source.lA, source.lB, source.lC, source.dteFirstAdded)
        output inserted.MachineID into @idTable;
        select @id = id from @idTable;
    end 
    go
    

    此示例将这两种情况分开,因为 T-SQL 查询应该从不尝试在一个查询中解析两种不同的解决方案(结果永远不可优化)。由于手头的两个任务(通过机器 ID 获取和通过度量获取)是完全独立的,因此应该是单独的过程,调用者应该调用适当的过程,而不是传递标志。这个例子展示了如何使用 MERGE 实现(可能)期望的结果,但是当然,正确最佳解决方案取决于实际的模式(表定义、索引和 cosntraints到位)和实际要求(不清楚如果标准已经匹配,而不是输出和@id,该程序应该做什么?)。

    通过消除 SERIALIZABLE 隔离,这不再保证死锁,但它仍然可能死锁。当然,解决死锁完全取决于未指定的模式,因此在这种情况下实际上无法提供死锁的解决方案。有一个锁定所有候选行的大锤(强制 UPDLOCK 甚至 TABLOCX),但是这样的解决方案会在大量使用时扼杀吞吐量,所以我不建议在不知道用例的情况下使用它。

    【讨论】:

    • 我如何指定列 A、B、C 应该是每个 fkContract 唯一的?
    • 所以我会对 fkContract、A、B 和 C 进行约束。然后我的存储过程基本上会插入(如果它已经存在则忽略错误),然后进行选择以查找主钥匙。如果没有交易,这一切都会发生。如果我使用约束,这是否更符合您的想法?
    • Serializable 隔离保证如果稍后在事务中再次运行读取(选择)将返回完全相同的行。因此,如果两个事务 T1 和 T2 同时运行您的 sp 中的原始 SELECT,它们将 both 保证此 SELECT 结果不会更改。说没有找到任何行,所以他们都会继续插入。先说T1插入。如果 T1 将成功插入和提交,并且 T2 将再次运行 SELECT,它将看到 T1 的插入。所以 T1 一定不能插入,否则会破坏 T2 完成的可序列化读取...
    • 现在你转身对 T2 应用同样的逻辑。它的 INSERT 不能成功和提交,因为如果是这样,那么这意味着如果 T1 重复 SELECT,它将看到 T2 的插入。所以 T1 被 T2 阻塞,T2 被 T1 阻塞,因此出现了死锁。可序列化隔离的实际实现是使用范围锁完成的,这就是为什么您会在死锁中看到 Range-S 和 Range-N 锁,但这就是实现方式 (2/2)
    • 关于返回值:客户端 API(ODBC、OleDB、ADO.Net、ADO、JDBC、PHP 驱动程序等)在它们支持过程返回值的方式之间存在很大差异。数据访问和 ORM 层(nHibernate、Linq2SQL、EF 等)也以非常不同的方式威胁返回值,并且有些框架甚至不提供对返回值的访问权限。另一个问题是DB中的数据类型发生了变化,一个int ID明年可以变成bigint,或者是decimal,但是程序返回类型是固定的(int),不能跟随变化。最后,OUTPUT 参数不能被忽略。总而言之,回报很烂
    【解决方案2】:

    摆脱交易。它并没有真正帮助你,而是在伤害你。这应该可以解决您的问题。

    【讨论】:

      【解决方案3】:

      这个 SQL 怎么样?它将现有数据的检查和插入移动到单个语句中。这样,当两个线程正在运行时,它们不会因为等待对方而死锁。充其量,线程二死锁等待线程一,但一旦线程一完成,线程二就可以运行。

      BEGIN TRY
      
        BEGIN TRAN SPFindContractMachine
      
        INSERT INTO Machines (fkContract, iA_Metric, iB_Metric, iC_Metric, dteFirstAdded)
          SELECT @fkContract, @lA, @lB, @lC, GETDATE()
            WHERE NOT EXISTS (
              SELECT * FROM Machines
              WHERE fkContract = @fkContract
              AND ((@bByID = 1 AND pkID = @nMachineID)
                   OR
                   (@bByID <> 1 AND iA_Metric = @lA AND iB_Metric = @lB AND iC_Metric = @lC))
      
        DECLARE @id INT
      
        SET @id = (
          SELECT pkID FROM Machines
          WHERE fkContract = @fkContract
          AND ((@bByID = 1 AND pkID = @nMachineID)
               OR
               (@bByID <> 1 AND iA_Metric = @lA AND iB_Metric = @lB AND iC_Metric = @lC)))
      
        COMMIT TRAN SPFindContractMachine
      
        RETURN @id
      
      END TRY
      BEGIN CATCH
        IF @@TRANCOUNT > 0
          ROLLBACK TRAN SPFindContractMachine
      END CATCH
      

      我还将这些 CASE 语句更改为 ORed 子句,只是因为它们对我来说更容易阅读。如果我回想一下我的 SQL 理论,ORing 可能会使这个查询慢一点。

      【讨论】:

      • 有趣.. 应该说 VALUES 而不是 SELECT 还是我读错了?我明天试试,看看有没有帮助
      • 那么通过将两个语句组合成 1 可以避免锁定?不能在后端解析成两个查询?
      • 我制作了一个测试数据库,上面有一个包含上述字段的测试表来测试这段代码,然后再发布。它确实有效。它是一个 INSERT INTO .. SELECT。要查看它是如何工作的,请尝试运行 SELECT .. WHERE NOT EXISTS 语句,您会发现如果行不存在,您将获得变量值,或者如果行 does 存在则没有值.两种说法变成了一种说法。对于两个语句,发生死锁是因为来自不同线程的语句插入两个语句之间。一个陈述,没有其他东西可以插话。
      • 哦,Remus Rusanu 的答案在 SQL Server 2008 中非常有效。我认为 MERGE 语句对于 SQL Server 2008 来说是新的。如果你还在 2005 年,我的 INSERT INTO .. SELECT .. WHERE NOT EXISTS() 会做你想做的事。 Remus 正确地建议将此代码分为两个过程,一个用于 ByMachineId,另一个用于 ByMetrics。
      【解决方案4】:

      我想知道在早期的 SELECT(s) 中添加一个 UPDLOCK 提示是否可以解决这个问题;它应该避免出现死锁情况,即阻止另一个 spud 对您将要变异的数据进行读锁。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-05-14
        相关资源
        最近更新 更多