更新并插入创建死锁的查询

时间:2011-09-02 12:37:49

标签: c# .net sql-server-2008 deadlock

我会尽可能详细地解释我的问题,我将不胜感激任何帮助/建议。我的问题是由两个查询(一次插入和一次更新)引起的死锁。我正在使用MS-SQL server 2008

我有两个使用相同数据库的应用程序:

  1. Web应用程序(在每次请求时,通过调用存储过程在Impressions表中插入多个记录)
  2. Windows服务(计算上一分钟内每分钟,每分钟完成的所有展示次数,并在通过存储过程计算的每次展示次数上设置一个标记)
  3. Web应用程序在不使用事务的情况下插入展示记录,而Windows服务应用程序在使用IsolationLevel.ReadUncommitted事务时计算展示次数。 Windows服务应用程序中的存储过程执行如下操作:

    Windows服务存储过程:

    循环显示isCalculated标记设置为false且日期&lt;的所有展示次数。 @now,增加连接到展示次数表的另一个表格中的计数器和其他数据,并在日期<&lt;的展示次数上将isCalculated标记设置为true。 @现在。因为这个存储过程非常大,没有必要粘贴它,这里是proc的缩短代码片段:

    DECLARE @nowTime datetime = convert(datetime, @now, 21) 
    DECLARE dailyCursor CURSOR FOR
    
    SELECT  Daily.dailyId, 
            Daily.spentDaily, 
            Daily.impressionsCountCache ,
            SUM(Impressions.amountCharged) as sumCharged, 
            COUNT(Impressions.impressionId) as countImpressions
    FROM    Daily INNER JOIN Impressions on Impressions.dailyId = Daily.dailyId
    WHERE   Impressions.isCharged=0 AND Impressions.showTime < @nowTime AND Daily.isActive = 1
    GROUP BY Daily.dailyId, Daily.spentDaily, Daily.impressionsCountCache
    
    OPEN dailyCursor
    
    DECLARE @dailyId int, 
            @spentDaily decimal(18,6), 
            @impressionsCountCache int, 
            @sumCharged decimal(18,6), 
            @countImpressions int
    
    FETCH NEXT FROM dailyCursor INTO @dailyId,@spentDaily, @impressionsCountCache, @sumCharged, @countImpressions
    
    WHILE @@FETCH_STATUS = 0
        BEGIN   
    
            UPDATE Daily 
            SET spentDaily= @spentDaily + @sumCharged, 
                impressionsCountCache = @impressionsCountCache + @countImpressions
            WHERE dailyId = @dailyId
    
            FETCH NEXT FROM dailyCursor INTO @dailyId,@spentDaily, @impressionsCountCache, @sumCharged, @countImpressions
        END
    CLOSE dailyCursor
    DEALLOCATE dailyCursor
    
    UPDATE Impressions 
    SET isCharged=1 
    WHERE showTime < @nowTime AND isCharged=0
    

    Web App存储过程:

    此过程非常简单,只需在表中插入记录即可。这是一个缩短的代码段:

    INSERT INTO Impressions 
    (dailyId, date, pageUrl,isCalculated) VALUES 
    (@dailyId, @date, @pageUrl, 0)
    

    守则

    调用这些存储过程的代码非常简单,它只是创建传递所需参数的SQL命令并执行它们

    //i send the date like this
    string date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", 
    CultureInfo.InvariantCulture);
    
    SqlCommand comm = sql.StoredProcedureCommand("storedProcName", 
    parameters, values);
    

    我经常遇到死锁(例外情况发生在网络应用程序中,而不是Windows服务),在使用SQL-Profiler之后,我发现可能因为这两个查询而发生死锁(我不知道)在分析探查器数据方面有很多经验。

    可以在此问题的底部找到从SQL Server Profiler收集的最新跟踪数据

    理论上,这两个存储过程应该能够一起工作,因为第一个存储过程一个接一个地插入日期= DateTime.Now,而第二个存储过程计算具有日期&lt; DateTime.Now。

    编辑:

    以下是Windows服务应用程序中运行的代码:

    SQL sql = new SQL();
    DateTime endTime = DateTime.Now;
    //our custom DAL class that opens a connection
    sql.StartTransaction(IsolationLevel.ReadUncommitted);
    try
    {
        List<string> properties = new List<string>() { "now" };
        List<string> values = new List<string>() { endTime.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) };
        SqlCommand comm = sql.StoredProcedureCommannd("ChargeImpressions", properties, values);
        comm.Transaction = sql.Transaction;
        ok = sql.CheckExecute(comm);
    }
    catch (Exception up)
    {
        ok = false;
        throw up;
    }
    finally
    {
        if (ok)
          sql.CommitTransaction();
        else
          sql.RollbackTransactions();
        CloseConn();
    }
    

    编辑:

    我按照Martin Smith的建议添加了两个表的索引:

    CREATE NONCLUSTERED INDEX [IDX_Daily_DailyId] ON [dbo].[Daily] 
    (
        [daily] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    GO
    

    CREATE NONCLUSTERED INDEX [IDX_Impressions_isCharged_showTime] ON [dbo].[Impressions] 
    (
        [isCharged] ASC,
        [showTime] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    GO
    

    现在没有例外,将在稍后报告

    编辑:

    不幸的是,这并没有解决死锁问题。我将在分析器中启动死锁跟踪,以查看死锁是否与以前相同。

    编辑:

    粘贴新的跟踪(对我看起来与前一个跟踪相同),无法捕获执行计划的屏幕(它太大了),但是here is the xml from the execution plan。这是执行的屏幕截图插入查询的计划:

    execution plan of the insert query

     <deadlock victim="process14e29e748">
      <process-list>
       <process id="process14e29e748" taskpriority="0" logused="952" waitresource="KEY: 6:72057594045071360 (f473d6a70892)" waittime="4549" ownerId="2507482845" transactionname="INSERT" lasttranstarted="2011-09-05T11:59:16.587" XDES="0x15bef83b0" lockMode="S" schedulerid="1" kpid="2116" status="suspended" spid="65" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2011-09-05T11:59:16.587" lastbatchcompleted="2011-09-05T11:59:16.587" clientapp=".Net SqlClient Data Provider"  hostpid="2200"  isolationlevel="snapshot (5)" xactid="2507482845" currentdb="6" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
         <frame procname="dbo.InsertImpression" line="27" stmtstart="2002" stmtend="2560" sqlhandle="0x03000600550e30512609e200529f00000100000000000000">
    INSERT INTO Impressions 
        (dailyId, languageId, showTime, pageUrl, amountCharged, age, ipAddress, userAgent, portalId, isCharged,isCalculated) VALUES 
        (@dailyId, @languageId, @showTime, @pageUrl, @amountCharged, @age, @ip, @userAgent, @portalId, 0, 0)     </frame>
        </executionStack>
        <inputbuf>
    Proc [Database Id = 6 Object Id = 1362103893]    </inputbuf>
       </process>
       <process id="process6c9dc8" taskpriority="0" logused="335684" waitresource="KEY: 6:72057594045464576 (5fcc21780b69)" waittime="4475" ownerId="2507482712" transactionname="transaction_name" lasttranstarted="2011-09-05T11:59:15.737" XDES="0x1772119b0" lockMode="U" schedulerid="2" kpid="3364" status="suspended" spid="88" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2011-09-05T11:59:15.737" lastbatchcompleted="2011-09-05T11:59:15.737" clientapp=".Net SqlClient Data Provider"  hostpid="1436" isolationlevel="read uncommitted (1)" xactid="2507482712" currentdb="6" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
        <executionStack>
         <frame procname="dbo.ChargeImpressions" line="60" stmtstart="4906" stmtend="5178" sqlhandle="0x03000600e3c5474f0609e200529f00000100000000000000">
    UPDATE Impressions 
        SET isCharged=1 
        WHERE showTime &amp;lt; @nowTime AND isCharged=0
    
        </frame>
        </executionStack>
        <inputbuf>
    Proc [Database Id = 6 Object Id = 1330103779]    </inputbuf>
       </process>
      </process-list>
      <resource-list>
       <keylock hobtid="72057594045071360" dbid="6" objectname="dbo.Daily" indexname="PK_Daily" id="lock14c6aab00" mode="X" associatedObjectId="72057594045071360">
        <owner-list>
         <owner id="process6c9dc8" mode="X"/>
        </owner-list>
        <waiter-list>
         <waiter id="process14e29e748" mode="S" requestType="wait"/>
        </waiter-list>
       </keylock>
       <keylock hobtid="72057594045464576" dbid="6" objectname="dbo.Impressions" indexname="IDX_Impressions_isCharged_showTime" id="lock14c901200" mode="X" associatedObjectId="72057594045464576">
        <owner-list>
         <owner id="process14e29e748" mode="X"/>
        </owner-list>
        <waiter-list>
         <waiter id="process6c9dc8" mode="U" requestType="wait"/>
        </waiter-list>
       </keylock>
      </resource-list>
     </deadlock>
    

    编辑:

    根据Jonathan Dickinson的建议:

    1. 我更改了存储过程(删除了光标),
    2. 我将IDX_Impressions_isCharged_showTime更改为不允许PAGE_LOCKS和
    3. 我在Windows服务应用程序中为@now属性添加了-1秒,以避免出现临界死锁情况。
    4. 更新

      查询执行时间在最后一次更改后减少了,但异常数没有。

      希望最后更新:

      Martin Smith提出的更改现在正在进行,插入查询现在使用非聚集索引,理论上这应该可以解决问题。现在没有报道任何例外(保持我的手指交叉)

3 个答案:

答案 0 :(得分:4)

避免游标,该查询不需要它们。 SQL 是一种命令式语言(这就是为什么它得名不好,因为每个人都将它用作一个) - 它是一种固定语言。

您可以做的第一件事是加快SQL的基本执行,减少解析/执行查询的时间意味着减少死锁的可能性:

  • 使用[dbo]前缀所有表格 - 这样可以减少分析阶段的30%。
  • 别名你的桌子 - 它会在规划阶段减少一小部分。
  • 引用标识符可以加快速度。
  • 这些是来自前SQL-PM的提示,然后才会有人对此提出异议。

您可以使用CTE获取要更新的数据,然后使用UPDATE ... FROM ... SELECT语句执行实际更新。这将比光标更快,因为与清洁设置操作相比,光标狗慢(即使是最快的“消防软管”光标也是如此)。更新花费的时间越少意味着死锁的可能性越小。 注意:我没有您的原始表,我无法对此进行验证 - 因此请根据开发数据库进行检查。

DECLARE @nowTime datetime = convert(datetime, @now, 21);

WITH [DailyAggregates] AS
(
    SELECT  
        [D].[dailyId] AS [dailyId],
        [D].[spentDaily] AS [spentDaily],
        [D].[impressionsCountCache] AS [impressionsCountCache],
        SUM([I].[amountCharged]) as [sumCharged],
        COUNT([I].[impressionId]) as [countImpressions]
        FROM [dbo].[Daily] AS [D]
            INNER JOIN [dbo].[Impressions] AS [I]
               ON [I].[dailyId] = [D].[dailyId]
        WHERE [I].[isCharged] = 0
          AND [I].[showTime] < @nowTime 
          AND [D].[isActive] = 1
    GROUP BY [D].[dailyId], [D].[spentDaily], [D].[impressionsCountCache]
)
UPDATE [dbo].[Daily]
    SET [spentDaily] = [A].[spentDaily] + [A].[sumCharged],
        [impressionsCountCache] = [A].[impressonsCountCache] + [A].[countImpressions]
    FROM [Daily] AS [D]
    INNER JOIN [DailyAggregates] AS [A]
       ON [D].[dailyId] = [A].[dailyId];

UPDATE [dbo].[Impressions]
SET [isCharged] = 1 
WHERE [showTime] < @nowTime 
  AND [isCharged] = 0;

此外,您可以禁止索引上的PAGE锁定,这将减少几行锁定整个页面的可能性(由于锁定升级,在整个页面刚刚锁定之前只需要锁定一定比例的行)

CREATE NONCLUSTERED INDEX [IDX_Impressions_isCharged_showTime] ON [dbo].[Impressions]              
(
    [showTime] ASC, -- I have a hunch that switching these around might have an effect.
    [isCharged] ASC  
)
WITH (ALLOW_PAGE_LOCKS = OFF)
ON [PRIMARY] 
GO

这只会减少陷入僵局的可能性。您可以尝试限制@now过去的日期(即today - 1 day),以确保插入的行不会落入更新谓词中;它有可能完全阻止僵局。

答案 1 :(得分:2)

您的Windows服务游标会更新Daily中需要X锁定的各行。在交易结束之前不会发布这些内容。

然后,您的网络应用会插入Impressions并在新插入的行上保持X锁定,同时等待S Daily锁定其中一行被其他进程锁定的。需要阅读此内容以验证FK约束。

然后,您的Windows服务会对Impressions上的更新执行U锁定其沿途扫描的行。没有索引允许它搜索行,因此此扫描包括Web应用程序添加的行。

所以

(1)您可以在Impressions上向showTime, isCharged添加复合索引,反之亦然(检查执行计划)以允许通过索引找到Windows服务将更新的行寻求而非全面扫描。

- 或

(2)您可以在Daily(DailyId)上添加冗余的非聚集索引。这将比聚簇索引更窄,因此FK验证可能会优先使用,而不需要在聚簇索引行上进行S锁定。

修改

免责声明:以下是基于假设和观察,而不是我发现的任何记录!

似乎想法(2)不能“按原样”工作。执行计划显示,无论现在有哪个更窄的索引,FK验证仍然会继续针对聚簇索引进行。 sys.foreign_keysreferenced_object_id, key_index_id,我推测验证目前总是会在其中列出的索引上进行,而查询优化工具目前不会考虑替代方案,但是没有找到任何记录此内容的内容。

在我删除并重新添加外键约束后,我发现sys.foreign_keys和查询计划中的相关值已更改为开始使用较窄的索引。

CREATE TABLE Daily(
    DailyId INT IDENTITY(1,1) PRIMARY KEY CLUSTERED  NOT NULL,
    Filler CHAR(4000) NULL,
) 

INSERT INTO Daily VALUES ('');


CREATE TABLE Impressions(
    ImpressionId INT IDENTITY(1,1) PRIMARY KEY NOT NULL,
    DailyId INT NOT NULL CONSTRAINT FK REFERENCES Daily (DailyId), 
    Filler CHAR(4000) NULL,
)

/*Execution Plan uses clustered index - There is no NCI*/ 
INSERT INTO Impressions VALUES (1,1) 

ALTER TABLE Daily ADD CONSTRAINT
    UQ_Daily UNIQUE NONCLUSTERED(DailyId) 

/*Execution Plan still use clustered index even after NCI created*/    
INSERT INTO Impressions VALUES (1,1) 

ALTER TABLE Impressions DROP CONSTRAINT FK
ALTER TABLE Impressions  WITH CHECK ADD  CONSTRAINT FK FOREIGN KEY(DailyId)
REFERENCES Daily (DailyId)    

/*Now Execution Plan now uses non clustered index*/    
INSERT INTO Impressions VALUES (1,1)    

Plan

答案 2 :(得分:0)

我确信其他答案建议的更改是需要的,因为例如在您的情况下不需要使用游标......从您提供的代码中甚至不需要WHILE太...

我不是SQL Server的人...如果我需要执行您的存储过程正在执行的操作,我会确保@nowTime = DateTime.Now.AddSeconds(-1)并将其编码为类似于以下内容:

BEGIN

UPDATE Daily D SET 
D.spentDaily= D.spentDaily + (SELECT SUM(I.amountCharged) FROM Impressions I WHERE I.isCharged=0 AND I.showTime < @nowTime AND I.DailyId = D.DailyId), 
D.impressionsCountCache = D.impressionsCountCache + (SELECT COUNT(I.impressionId) FROM Impressions I WHERE I.isCharged=0 AND I.showTime < @nowTime AND I.DailyId = D.DailyId)
WHERE D.DailyId IN (SELECT I.DailyId FROM Impressions I WHERE I.isCharged=0 AND I.showTime < @nowTime AND I.DailyId = D.DailyId) AND D.isActive = 1;

UPDATE Impressions I SET
I.isCharged=1 
WHERE I.showTime < @nowTime AND I.isCharged=0;

COMMIT;

END

即使在高负载下,INSERT UPDATE / DELETE上的任何并行Impressions都没有任何死锁问题(尽管那是Oracle)... HTH