SQL Server 2005 中的死锁!两个实时批量更新正在战斗。为啥?

Posted

技术标签:

【中文标题】SQL Server 2005 中的死锁!两个实时批量更新正在战斗。为啥?【英文标题】:Deadlock in SQL Server 2005! Two real-time bulk upserts are fighting. WHY?SQL Server 2005 中的死锁!两个实时批量更新正在战斗。为什么? 【发布时间】:2011-03-02 15:53:54 【问题描述】:

这是场景:

我有一个名为 MarketDataCurrent (MDC) 的表,它可以实时更新股票价格。

我有一个名为“LiveFeed”的进程,它从网络中读取价格流,对插入进行排队,并使用“批量上传到临时表然后插入/更新到 MDC 表”。 (批量更新)

我有另一个进程,它会读取这些数据,计算其他数据,然后使用类似的 BulkUpsert 存储过程将结果保存回同一个表中。

第三,有大量用户运行 C# Gui 轮询 MDC 表并从中读取更新。

现在,在数据快速变化的那一天,事情运行得相当顺利,但是,在市场交易时间之后,我们最近开始看到数据库中出现越来越多的死锁异常,现在我们看到 10-一天20个。这里要注意的重要一点是,当值不变时,这些情况就会发生。

以下是所有相关信息:

表定义:

CREATE TABLE [dbo].[MarketDataCurrent](
 [MDID] [int] NOT NULL,
 [LastUpdate] [datetime] NOT NULL,
 [Value] [float] NOT NULL,
 [Source] [varchar](20) NULL, 
CONSTRAINT [PK_MarketDataCurrent] PRIMARY KEY CLUSTERED 
(
 [MDID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

-

我有一个正在运行的 Sql Profiler 跟踪,正在捕获死锁,这是所有图表的样子。

进程 258 被重复调用以下 'BulkUpsert' 存储过程,而 73 正在调用下一个:

ALTER proc [dbo].[MarketDataCurrent_BulkUpload]
 @updateTime datetime,
 @source varchar(10)
as

begin transaction

update c with (rowlock) set LastUpdate = getdate(), Value = t.Value, Source = @source 
from MarketDataCurrent c INNER JOIN #MDTUP t ON c.MDID = t.mdid
where c.lastUpdate < @updateTime
and   c.mdid not in (select mdid from MarketData where LiveFeedTicker is not null     and     PriceSource like 'LiveFeed.%')
and   c.value <> t.value

insert  into MarketDataCurrent
with (rowlock)
select  MDID, getdate(), Value, @source from #MDTUP 
where mdid not in (select mdid from MarketDataCurrent with (nolock))
and  mdid not in (select mdid from MarketData where LiveFeedTicker is not null     and PriceSource like 'LiveFeed.%')

commit

还有一个:

ALTER PROCEDURE [dbo].[MarketDataCurrent_LiveFeedUpload] 
AS
begin transaction

 -- Update existing mdid
 UPDATE c WITH (ROWLOCK) SET LastUpdate = t.LastUpdate, Value = t.Value, Source = t.Source 
 FROM MarketDataCurrent c INNER JOIN #TEMPTABLE2 t ON c.MDID = t.mdid;

 -- Insert new MDID
 INSERT INTO MarketDataCurrent with (ROWLOCK) SELECT * FROM #TEMPTABLE2 
 WHERE MDID NOT IN (SELECT MDID FROM MarketDataCurrent with (NOLOCK))

 -- Clean up the temp table
 DELETE #TEMPTABLE2

commit

为了澄清,这些临时表是由同一连接上的 C# 代码创建的,并使用 C# SqlBulkCopy 类填充。

在我看来,它似乎在表的 PK 上死锁,所以我尝试删除该 PK 并改为切换到唯一约束,但这将死锁数量增加了 10 倍。

我完全不知道如何处理这种情况,并且愿意接受任何建议。

求助!!


响应对 XDL 的请求,这里是:

<deadlock-list>
 <deadlock victim="processc19978">
  <process-list>
   <process id="processaf0b68" taskpriority="0" logused="0" waitresource="KEY: 6:72057594090487808 (d900ed5a6cc6)" waittime="718" ownerId="1102128174" transactionname="user_transaction" lasttranstarted="2010-06-11T16:30:44.750" XDES="0xffffffff817f9a40" lockMode="U" schedulerid="3" kpid="8228" status="suspended" spid="73" sbid="0" ecid="0" priority="0" transcount="2" lastbatchstarted="2010-06-11T16:30:44.750" lastbatchcompleted="2010-06-11T16:30:44.750" clientapp=".Net SqlClient Data Provider" hostname="RISKAPPS_VM" hostpid="3836" loginname="RiskOpt" isolationlevel="read committed (2)" xactid="1102128174" currentdb="6" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
    <executionStack>
     <frame procname="MKP_RISKDB.dbo.MarketDataCurrent_BulkUpload" line="28" stmtstart="1062" stmtend="1720" sqlhandle="0x03000600a28e5e4ef4fd8e00849d00000100000000000000">
UPDATE c WITH (ROWLOCK) SET LastUpdate = getdate(), Value = t.Value, Source = @source 
FROM MarketDataCurrent c INNER JOIN #MDTUP t ON c.MDID = t.mdid
WHERE c.lastUpdate &lt; @updateTime
and   c.mdid not in (select mdid from MarketData where BloombergTicker is not null and PriceSource like &apos;Blbg.%&apos;)
and   c.value &lt;&gt; t.value     </frame>
     <frame procname="adhoc" line="1" stmtstart="88" sqlhandle="0x01000600c1653d0598706ca7000000000000000000000000">
exec MarketDataCurrent_BulkUpload @clearBefore, @source     </frame>
     <frame procname="unknown" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000">
unknown     </frame>
    </executionStack>
    <inputbuf>
(@clearBefore datetime,@source nvarchar(10))exec MarketDataCurrent_BulkUpload @clearBefore, @source    </inputbuf>
   </process>
   <process id="processc19978" taskpriority="0" logused="0" waitresource="KEY: 6:72057594090487808 (74008e31572b)" waittime="718" ownerId="1102128228" transactionname="user_transaction" lasttranstarted="2010-06-11T16:30:44.780" XDES="0x380be9d8" lockMode="U" schedulerid="5" kpid="8464" status="suspended" spid="248" sbid="0" ecid="0" priority="0" transcount="2" lastbatchstarted="2010-06-11T16:30:44.780" lastbatchcompleted="2010-06-11T16:30:44.780" clientapp=".Net SqlClient Data Provider" hostname="RISKBBG_VM" hostpid="4480" loginname="RiskOpt" isolationlevel="read committed (2)" xactid="1102128228" currentdb="6" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
    <executionStack>
     <frame procname="MKP_RISKDB.dbo.MarketDataCurrentBlbgRtUpload" line="14" stmtstart="840" stmtend="1220" sqlhandle="0x03000600005f9d24c8878f00849d00000100000000000000">
UPDATE c WITH (ROWLOCK) SET LastUpdate = t.LastUpdate, Value = t.Value, Source = t.Source 
        FROM MarketDataCurrent c INNER JOIN #TEMPTABLE2 t ON c.MDID = t.mdid;

        -- Insert new MDID     </frame>
     <frame procname="adhoc" line="1" sqlhandle="0x010006004a58132228bf8d73000000000000000000000000">
MarketDataCurrentBlbgRtUpload     </frame>
    </executionStack>
    <inputbuf>
MarketDataCurrentBlbgRtUpload    </inputbuf>
   </process>
  </process-list>
  <resource-list>
   <keylock hobtid="72057594090487808" dbid="6" objectname="MKP_RISKDB.dbo.MarketDataCurrent" indexname="PK_MarketDataCurrent" id="lock5ba77b00" mode="U" associatedObjectId="72057594090487808">
    <owner-list>
     <owner id="processc19978" mode="U"/>
    </owner-list>
    <waiter-list>
     <waiter id="processaf0b68" mode="U" requestType="wait"/>
    </waiter-list>
   </keylock>
   <keylock hobtid="72057594090487808" dbid="6" objectname="MKP_RISKDB.dbo.MarketDataCurrent" indexname="PK_MarketDataCurrent" id="lock65dca340" mode="U" associatedObjectId="72057594090487808">
    <owner-list>
     <owner id="processaf0b68" mode="U"/>
    </owner-list>
    <waiter-list>
     <waiter id="processc19978" mode="U" requestType="wait"/>
    </waiter-list>
   </keylock>
  </resource-list>
 </deadlock>
</deadlock-list>

【问题讨论】:

如果删除显式 rowlock 和 nolock 说明符会发生什么?您的查询视图中是否提到了任何数据源而不是表,如果是这样,您能否发布视图源?谢谢。 所有数据源都不是视图。实际上,添加显式 rowlock 和 nolocks 有助于减少死锁的数量。 最好发布实际的死锁 XDL,而不是图像。图片可能具有欺骗性...rusanu.com/2010/05/12/the-puzzle-of-u-locks-in-deadlock-graphs 那确实是一篇有趣的文章,现在我有一些额外的东西要注意,但是从这个 XDL 中可以看出,它们似乎都是 U 锁。我会再看一些,看看能不能找到一个不是的。 +1 只是为了说明问题的详细记录,如果没有别的 【参考方案1】:

死锁似乎是密钥访问顺序上的直接死锁。一个简单的解释是两个批量更新操作之间更新的密钥重叠。

不过,一个不太简单的解释是,在 SQL Server(以及其他服务器)中,锁定的键是散列,并且存在(相当重要的)散列冲突概率。这可以解释为什么您最近看到的死锁比以前更多:只是您的数据量增加了,因此冲突概率增加了。如果这看起来很深奥且不太可能,请继续阅读%%lockres%% collision probability magic marker: 16,777,215,以及从那里链接的文章。概率高得惊人,对于 完美 密钥分配,仅在大约 16M 插入后,您就有 50% 的冲突概率。对于正常的、现实世界的密钥分布,您仅在几千次插入时就有显着的冲突概率。不幸的是,没有解决方法。如果这确实是问题,您唯一的解决方案是减少批次的大小(#temp 表的大小),以降低冲突概率。或者处理死锁并重试......无论如何你都必须这样做,但至少你可以处理更少个死锁。

【讨论】:

到目前为止,这种见解非常有帮助。在我想出一种方法将这些死锁减少到可接受的水平之前,我还不能接受任何答案,但我认为这是在正确的轨道上。你的文章链接到这个,这很有趣:consultingblogs.emc.com/jamesrowlandjones/archive/2009/05/28/… 他提到了这个问题的三种可能的解决方案,其中 #1 是 1。将你的密钥更改为基于代理整数的密钥但这正是我所拥有的。此表中的PK是唯一的int,仅此而已 我实际上不同意詹姆斯的观点。将键更改为 int 代理键将完全没有任何作用,8 字节的 int 仍将散列到锁定资源大小,即 6 字节。恕我直言。唯一合理的方法是减少批量大小。 刚跑:从 MarketDataCurrent 组中选择 %%lockres%% 作为 LockHas,count() 由 %%lockres%% 具有 count() > 1 (在中间天)并得到 0 行!我们的桌子实际上并没有那么大。总共约 32,000 行(它应该是非常轻量级的,并且只有实时数据)因此,使用唯一的 int id pk,以及高达 250,000 个哈希键冲突范围内的值似乎几乎不可能作为解释。我会在下一次死锁之后再次运行此查询,以查看当时是否有任何问题,但我不希望它们。 另外,FWIW,一个进程的批处理大小约为 200-300 行,另一个进程为 5000。 其他问题,你知道我如何从上面的 XDL 中解决死锁的行的实际 ID 吗?【参考方案2】:

它发生在主要工作时间之后,数据没有变化,而且最近才开始。服务器最近有什么变化吗?我怀疑一些新的数据库维护工作可能会干扰。

顺便说一句,如果您知道市场关闭并且数据没有变化,为什么您的流程仍在运行?

【讨论】:

我管理服务器并且知道在它上面运行的一切。有一个 SQL DTSS 备份作业,每晚都会对同一个物理硬盘驱动器进行完整备份,然后另一个 SQL 服务器上的另一个 DTSS 从我服务器的本地驱动器复制到网络“快照服务器” 我对根本不更新的数据过于简单化了。大多数市场关闭,但外汇实际上是 24/5 开放,因此我们需要处理该市场。【参考方案3】:

我想回答我在评论中提出的一个问题,即,

“如何识别锁定的行?”。

在下面的死锁 XDL 中,在两个正在锁定的“进程”节点上,有一个 waitresource 属性。在这种情况下:

waitresource="KEY: 6:72057594090487808 (d4005c04b35f)

waitresource="KEY: 6:72057594090487808 (b00072ea4ffd)

使用Remus指向的%%lockres%%关键字,

select %%lockres%%, * from MarketDataCurrent 
   where %%lockres%% in ('(d4005c04b35f)', '(b00072ea4ffd)')

这产生了冲突的两行。它们确实是唯一的 id,并且没有冲突。我仍然不知道为什么我会在这里陷入僵局,但我越来越接近了。

我会注意到,这两个 id 都应该只来自 LiveFeed 程序,但同样,更新中有一个子句应该从另一端的实际更新中过滤掉这一行。

<deadlock-list>
 <deadlock victim="processffffffff8f5872e8">
  <process-list>
   <process id="process8dcb68" taskpriority="0" logused="1256" waitresource="KEY: 6:72057594090487808 (d4005c04b35f)" waittime="1906" ownerId="1349627324" transactionname="user_transaction" lasttranstarted="2010-06-16T16:50:04.727" XDES="0x424e6258" lockMode="U" schedulerid="2" kpid="1004" status="suspended" spid="683" sbid="0" ecid="0" priority="0" transcount="2" lastbatchstarted="2010-06-16T16:50:04.727" lastbatchcompleted="2010-06-16T16:50:04.727" clientapp=".Net SqlClient Data Provider" hostname="RISKAPPS_VM" hostpid="2600" loginname="RiskOpt" isolationlevel="read committed (2)" xactid="1349627324" currentdb="6" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
    <executionStack>
     <frame procname="MKP_RISKDB.dbo.MarketDataCurrent_BulkUpload" line="28" stmtstart="1062" stmtend="1720" sqlhandle="0x03000600a28e5e4ef4fd8e00849d00000100000000000000">
        UPDATE c WITH (ROWLOCK) SET LastUpdate = getdate(), Value = t.Value, Source = @source 
        FROM MarketDataCurrent c INNER JOIN #MDTUP t ON c.MDID = t.mdid
        WHERE c.lastUpdate &lt; @updateTime
        and   c.mdid not in (select mdid from MarketData where BloombergTicker is not null and PriceSource like &apos;Blbg.%&apos;)
        and   c.value &lt;&gt; t.value     </frame>
             <frame procname="adhoc" line="1" stmtstart="88" sqlhandle="0x01000600c1653d0598706ca7000000000000000000000000">
        exec MarketDataCurrent_BulkUpload @clearBefore, @source     </frame>
     <frame procname="unknown" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000">unknown</frame>
    </executionStack>
    <inputbuf>(@clearBefore datetime,@source nvarchar(10))exec MarketDataCurrent_BulkUpload @clearBefore, @source</inputbuf>
   </process>
   <process id="processffffffff8f5872e8" taskpriority="0" logused="0" waitresource="KEY: 6:72057594090487808 (b00072ea4ffd)" waittime="1921" ownerId="1349627388" transactionname="user_transaction" lasttranstarted="2010-06-16T16:50:04.757" XDES="0x289ea040" lockMode="U" schedulerid="5" kpid="11192" status="suspended" spid="382" sbid="0" ecid="0" priority="0" transcount="2" lastbatchstarted="2010-06-16T16:50:04.757" lastbatchcompleted="2010-06-16T16:50:04.757" clientapp=".Net SqlClient Data Provider" hostname="RISKBBG_VM" hostpid="2452" loginname="RiskOpt" isolationlevel="read committed (2)" xactid="1349627388" currentdb="6" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
    <executionStack>
     <frame procname="MKP_RISKDB.dbo.MarketDataCurrentBlbgRtUpload" line="14" stmtstart="840" stmtend="1220" sqlhandle="0x03000600005f9d24c8878f00849d00000100000000000000">
        UPDATE c WITH (ROWLOCK) SET LastUpdate = t.LastUpdate, Value = t.Value, Source = t.Source 
        FROM MarketDataCurrent c INNER JOIN #TEMPTABLE2 t ON c.MDID = t.mdid;
    </frame>
     <frame procname="adhoc" line="1" sqlhandle="0x010006004a58132228bf8d73000000000000000000000000">
        MarketDataCurrentBlbgRtUpload     </frame>
    </executionStack>
    <inputbuf>
        MarketDataCurrentBlbgRtUpload    </inputbuf>
   </process>
  </process-list>
  <resource-list>
   <keylock hobtid="72057594090487808" dbid="6" objectname="MKP_RISKDB.dbo.MarketDataCurrent" indexname="PK_MarketDataCurrent" id="lock409d32c0" mode="U" associatedObjectId="72057594090487808">
    <owner-list>
     <owner id="processffffffff8f5872e8" mode="U"/>
    </owner-list>
    <waiter-list>
     <waiter id="process8dcb68" mode="U" requestType="wait"/>
    </waiter-list>
   </keylock>
   <keylock hobtid="72057594090487808" dbid="6" objectname="MKP_RISKDB.dbo.MarketDataCurrent" indexname="PK_MarketDataCurrent" id="lock706647c0" mode="U" associatedObjectId="72057594090487808">
    <owner-list>
     <owner id="process8dcb68" mode="U"/>
    </owner-list>
    <waiter-list>
     <waiter id="processffffffff8f5872e8" mode="U" requestType="wait"/>
    </waiter-list>
   </keylock>
  </resource-list>
 </deadlock>
</deadlock-list>

【讨论】:

【参考方案4】:

经过近两年烦人的死锁警告电子邮件,我终于解决了这个问题。

我通过在我的竞争插入上使用 FULL TABLE LOCKING 解决了这个问题。我曾尝试将锁定减少到行级别,但锁定正在升级到表级别。最后,我认为该表足够小,即使有很多用户每秒都在对其进行读写,但为了数据一致性,我愿意为全锁带来很小的性能损失。

此外,使用 MERGE 将插入/更新组合到一个原子语句允许我执行此操作。

这是已解决的生产代码(它有效!):

declare @date datetime;
set @date = getdate();

merge marketdatacurrent with (tablockx) as mdc

using #MDTUP as upload
    on mdc.MDID = upload.MDID

when matched then
    update
    set mdc.lastupdate = @date,
        mdc.value = upload.value,
        mdc.source = @source

when not matched then
    insert ( mdid, lastupdate, value, source )
    values ( upload.mdid, @date, upload.value, @source);

【讨论】:

以上是关于SQL Server 2005 中的死锁!两个实时批量更新正在战斗。为啥?的主要内容,如果未能解决你的问题,请参考以下文章

诊断SQL Server 2005中的死锁

sql server 2005 死锁在生产中超时,而不是在测试环境中:为啥?

SQL Server 2005 密钥死锁

sql server2005的死锁

SQL Server中解决死锁的新方法介绍

access和sql server实时同步