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

Atz*_*oya 13 .net c# deadlock sql-server-2008

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

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

  1. Web应用程序(在每个请求中,通过调用存储过程在Impressions表中插入多个记录)
  2. Windows服务(计算上一分钟内每分钟,每分钟完成的所有展示次数,并在通过存储过程计算的每次展示次数上设置一个标记)

Web应用程序在不使用事务的情况下插入展示记录,而Windows服务应用程序在使用IsolationLevel.ReadUncommitted事务时计算展示次数.Windows服务应用程序中的存储过程执行以下操作:

Windows服务存储过程:

循环通过将isCalculated标志设置为false并且日期<@now的所有展示,增加计数器和连接到展示表的另一个表中的其他数据,并isCalculated在具有日期<@now的展示中将标记设置为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
Run Code Online (Sandbox Code Playgroud)

Web App存储过程:

这个过程非常简单,它只是在表中插入记录.这是一个缩短的代码段:

INSERT INTO Impressions 
(dailyId, date, pageUrl,isCalculated) VALUES 
(@dailyId, @date, @pageUrl, 0)
Run Code Online (Sandbox Code Playgroud)

代码

调用这些存储过程的代码非常简单,它只是创建传递所需参数并执行它们的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);
Run Code Online (Sandbox Code Playgroud)

我经常遇到死锁(例外情况发生在Web应用程序中,而不是Windows服务),并且在使用SQL-Profiler之后,我发现死锁可能是因为这两个查询而发生的(我没有分析分析器数据的丰富经验).

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

理论上,这两个存储过程应该能够一起工作,因为第一个存储过程一个接一个地插入日期= DateTime.Now,第二个计算具有日期<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();
}
Run Code Online (Sandbox Code Playgroud)

编辑:

我按照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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

编辑:

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

编辑:

粘贴新的跟踪(对我来说它看起来和前一个一样),无法捕获执行计划的屏幕(它太大了)但这里是执行计划中的xml.这里是执行的截图插入查询的计划:

插入查询的执行计划

 <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>
Run Code Online (Sandbox Code Playgroud)

编辑:

根据Jonathan Dickinson的建议:

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

更新:

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

希望最后更新:

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

Mar*_*ith 3

Daily您的 Windows 服务游标会更新它锁定的各个行X。这些在交易结束之前不会被释放。

然后,您的 Web 应用程序会执行插入操作,并在新插入的行上Impressions保持X锁定,同时等待其他进程锁定的S其中一行上的锁定。Daily它需要读取此内容来验证 FK 约束。

然后,您的 Windows 服务会对其扫描的行Impressions执行更新操作。U没有索引允许它查找行,因此此扫描包括 Web 应用程序添加的行。

所以

(1) 您可以添加一个复合索引,Impressions反之亦然showTime, isCharged(检查执行计划),以允许通过索引查找而不是完整扫描来找到 Windows 服务将更新的行。

-或者

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

编辑

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

看来想法(2)“按原样”不起作用。执行计划显示,尽管现在有更窄的索引可用,但 FK 验证仍然继续针对聚集索引进行。sys.foreign_keys有列referenced_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)    
Run Code Online (Sandbox Code Playgroud)

计划