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

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

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

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

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

Windows服务存储过程:

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

我经常遇到死锁(例外情况发生在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 properties = new List() { "now" }; List values = new List() { 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 

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

编辑:

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

编辑:

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

插入查询的执行计划

       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)    Proc [Database Id = 6 Object Id = 1362103893]      UPDATE Impressions SET isCharged=1 WHERE showTime &lt; @nowTime AND isCharged=0    Proc [Database Id = 6 Object Id = 1330103779]                       

编辑:

根据Jonathan Dickinson的建议:

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

更新:

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

希望最后更新:

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

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

然后,您的Web应用程序执行Insert into Impressions并在新插入的行上保持X锁定,同时等待在其他进程锁定的Daily行中的某一行上的S锁定。 它需要读取此内容以validationFK约束。

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

所以

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

-要么

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

编辑

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

看来这个想法(2)不能“按原样”起作用。 执行计划显示,对于聚簇索引,FKvalidation仍然会继续发生,而不管现在是否有更窄的索引。 sys.foreign_keyssys.foreign_keys referenced_object_id, key_index_idreferenced_object_id, key_index_id我推测validation目前总是会在那里列出的索引上进行,而查询优化器目前不会考虑替代方案,但是没有找到任何记录这一点的内容。

我发现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) 

计划

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

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

  • 使用[dbo]所有表格添加前缀 – 这样可以减少分析阶段的30%。
  • 对表格进行别名 – 它会在计划阶段之前减少一小部分。
  • 引用标识符可能会加快速度。
  • 在有人决定对此提出异议之前,这些是来自前SQL-PM的提示。

您可以使用CTE来获取要更新的数据,然后使用UPDATE ... FROM ... SELECT语句来执行实际更新。 这将比光标更快,因为与干净的设置操作(即使是像你的最快的’消防软管’光标)相比,游标也会变慢 。 花在更新上的时间越少意味着死锁的可能性越小。 注意:我没有原始表,我无法对此进行validation – 因此请根据开发数据库进行检查。

 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 ),以确保插入的行不属于更新谓词; 它有可能完全阻止僵局。

我确信其他答案建议的更改是需要的,因为例如在您的情况下不需要使用光标……从您提供的代码中甚至不需要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 

即使是高负载也没有任何与Impressions这样的并行INSERT / UPDATE / DELETE死锁问题(虽然那是Oracle)... HTH