我有有关死锁的数据,但我不明白它们为什么会发生。

7
我在我的大型Web应用程序中收到了很多死锁。 如何自动重新运行死锁事务?(ASP.NET MVC/SQL Server) 我想重新运行死锁事务,但被告知要摆脱死锁——这比尝试捕捉死锁要好得多。
所以我花了一整天的时间使用SQL Profiler、设置跟踪键等。这就是我得到的结果。
有一个Users表。我有一个非常高可用页面,其中包含以下查询(不是唯一的查询,但它是引起麻烦的查询之一)。
UPDATE Users
SET views = views + 1
WHERE ID IN (SELECT AuthorID FROM Articles WHERE ArticleID = @ArticleID)

然后在所有页面中都有以下查询:

User = DB.Users.SingleOrDefault(u => u.Password == password && u.Name == username);

我是从Cookie中获取用户的。

很多时候会发生死锁,并选择第二个Linq-to-SQL查询作为受害者,因此它不会运行,我的网站用户会看到错误屏幕。

这是通过SQL Profiler捕获的.XDL图表中的信息(这只是第一个死锁,而不是唯一的死锁。整个列表都非常庞大):

<deadlock-list>
    <deadlock victim="process824df048">
        <process-list>
            <process id="process824df048" taskpriority="0" logused="0" waitresource="PAGE: 7:1:13921" waittime="1830" ownerId="91418" transactionname="SELECT" lasttranstarted="2010-05-31T12:17:37.663" XDES="0x868175e0" lockMode="S" schedulerid="2" kpid="5076" status="suspended" spid="72" sbid="0" ecid="2" priority="0" trancount="0" lastbatchstarted="2010-05-31T12:17:37.663" lastbatchcompleted="2010-05-31T12:17:37.663" clientapp=".Net SqlClient Data Provider" hostname="WIN-S41KV2CLS67" hostpid="6920" isolationlevel="read committed (2)" xactid="91418" currentdb="7" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
                <executionStack>
                    <frame procname="adhoc" line="1" stmtstart="74" sqlhandle="0x02000000de1cb30b5b2e40e31ffb345af3c7529430b559c2">
*password-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------     </frame>
                    <frame procname="unknown" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000">
unknown     </frame>
                </executionStack>
                <inputbuf>
                </inputbuf>
            </process>
            <process id="process8765fb88" taskpriority="0" logused="216" waitresource="PAGE: 7:1:14196" waittime="1822" ownerId="91408" transactionname="UPDATE" lasttranstarted="2010-05-31T12:17:37.640" XDES="0x86978e90" lockMode="IX" schedulerid="2" kpid="5216" status="suspended" spid="73" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2010-05-31T12:17:37.557" lastbatchcompleted="2010-05-31T12:17:37.557" clientapp=".Net SqlClient Data Provider" hostname="WIN-S41KV2CLS67" hostpid="6920" loginname="sdfkj93jks9sl" isolationlevel="read committed (2)" xactid="91408" currentdb="7" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
                <executionStack>
                    <frame procname="database.dbo.UpdateUserStats" line="31" stmtstart="1794" stmtend="2088" sqlhandle="0x03000700bac8836333e58f00879d00000100000000000000">
UPDATE Users
    SET Views = Views + 1
    WHERE ID IN (SELECT AuthorID FROM Articles WHERE ArticleID = @ArticleID)     </frame>
                    <frame procname="adhoc" line="1" stmtstart="84" sqlhandle="0x01000700b7c78e0760dd3f81000000000000000000000000">
EXEC @RETURN_VALUE = [dbo].[UpdateUserStats] @UserID = @p0    </frame>
                    <frame procname="unknown" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000">
unknown     </frame>
                </executionStack>
                <inputbuf>
(@p0 int,@RETURN_VALUE int output)EXEC @RETURN_VALUE = [dbo].[UpdateUserStats] @UserID = @p0   </inputbuf>
            </process>
            <process id="process86ce0988" taskpriority="0" logused="10000" waittime="1806" schedulerid="1" kpid="2604" status="suspended" spid="72" sbid="0" ecid="0" priority="0" trancount="0" lastbatchstarted="2010-05-31T12:17:37.663" lastbatchcompleted="2010-05-31T12:17:37.663" clientapp=".Net SqlClient Data Provider" hostname="WIN-S41KV2CLS67" hostpid="6920" loginname="sdfkj93jks9sl" isolationlevel="read committed (2)" xactid="91418" currentdb="7" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
                <executionStack>
                    <frame procname="adhoc" line="1" stmtstart="74" sqlhandle="0x02000000de1cb30b5b2e40e31ffb345af3c7529430b559c2">
*password-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------     </frame>
                    <frame procname="unknown" line="1" sqlhandle="0x000000000000000000000000000000000000000000000000">
unknown     </frame>
                </executionStack>
                <inputbuf>
*password--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------    </inputbuf>
            </process>
        </process-list>
        <resource-list>
            <pagelock fileid="1" pageid="13921" dbid="7" objectname="database.dbo.Users" id="lock85535c80" mode="IX" associatedObjectId="72057594046382080">
                <owner-list>
                    <owner id="process8765fb88" mode="IX"/>
                </owner-list>
                <waiter-list>
                    <waiter id="process824df048" mode="S" requestType="wait"/>
                </waiter-list>
            </pagelock>
            <pagelock fileid="1" pageid="14196" dbid="7" objectname="database.dbo.Users" id="lock8469f980" mode="SIU" associatedObjectId="72057594046382080">
                <owner-list>
                    <owner id="process86ce0988" mode="S"/>
                </owner-list>
                <waiter-list>
                    <waiter id="process8765fb88" mode="IX" requestType="convert"/>
                </waiter-list>
            </pagelock>
            <exchangeEvent id="Pipe894b0680" WaitType="e_waitPipeGetRow" nodeId="0">
                <owner-list>
                    <owner id="process824df048"/>
                </owner-list>
                <waiter-list>
                    <waiter id="process86ce0988"/>
                </waiter-list>
            </exchangeEvent>
        </resource-list>
    </deadlock>

我阅读了很多关于死锁的内容...但我不明白为什么这会导致死锁。

显然,这两个查询经常运行。至少每秒一次。甚至可能更频繁(在线用户数为300-400)。因此,它们可以很容易地同时运行,但为什么会导致死锁呢?请帮忙解答。

谢谢


1
你使用的是 SQL2005 或之后的版本吗?如果是的话,你是否从 SQL Profiler 中获取了死锁图?另外,你知道你的查询正在运行在哪个事务隔离级别下吗? - Martin Smith
是的,我得到了这个图表。这就是我获取这些信息的地方。我不知道事务隔离的任何内容。我在哪里可以查看它? - Alex
1
在分析器跟踪中右键单击死锁图事件,选择提取事件数据,将其保存为 XML。然后在记事本中打开它并搜索“isolationlevel”。 - Martin Smith
我刚刚检查了一下,它总是“读取已提交(2)”。 - Alex
3个回答

13

你需要捕获死锁图。附加Profiler并捕获Deadlock Graph Event类。保存.XDL图并将该信息添加到您的帖子中。

在此之前,很明显您的DB.Users.SingleOrDefault查询至少需要一个Name索引,如果没有Name和Password的话:

CREATE INDEX idxUsersNamePassword on Users(Name,Password);

我希望用户已经在ID上拥有索引,并且文章也有一篇覆盖作者ID的ArticleID索引。假设Users.ID和Articles.ArticleID是它们各自表格中的PK,它们可能是相应的聚集键,所以这是正确的。不过最好再确认一下。
另外,正如我在你之前的帖子中已经回答过一次并且你决定继续而没有回答,你应该考虑开启快照隔离
ALTER DATABASE ... SET READ_COMMITTED_SNAPSHOT ON

除此之外,以明文形式存储密码是一个重大的失败。
死锁信息后更新
有三个进程(请求):
A)...F048正在运行SELECT ... FROM Users WHERE Password = ... and Name = ... B)...0988正在运行SELECT ... FROM Users WHERE Password = ... and Name = ... C)...FB88正在运行UPDATE ... 死锁循环如下:
1. C等待页面IX锁,被A的S锁阻止 2. B等待页面S锁,被C的IX锁阻塞 3. A等待并行交换资源,被B阻塞
因此,循环为C->A->B->C。
从涉及到的两个SELECT决定使用并行计划和页面锁的事实来看,它们会对整个Users表进行端到端扫描。因此,问题就像我预测的那样,是由于缺少在用户上(Name,Password)上的索引,导致查询扫描了太多数据。添加索引将把SELECT转换为Nc索引上的直接查找和聚集索引上的查找,并且这将显着减少与UPDATE重叠的窗口。现在,由于每个SELECT都保证读取每一行,因此UPDATE几乎肯定会与所有SELECT发生冲突。
添加索引将缓解即时问题。使用快照隔离会掩盖该问题,因为除非添加(Name, Password)索引,否则仍将进行端到端扫描。或者仅(Name)也可能奏效。
对于未来的可伸缩性,每次页面查看都更新Views列将行不通。延迟更新,批量聚合计数更新,垂直分区用户表并删除Views列是可行的替代方案。

1
抱歉,我忘记接受你的回答了。是的,我打算更新密码存储系统。我会在我的问题中加入.XDL信息。 - Alex
非常感谢您提供如此详细的答案!我从未遇到过索引任何与主键不同的列。我需要做什么来添加(名称,密码)索引?我在表格设计师中找到了索引/键窗口。它有一个PK_Users键,我可以为用户名添加IX_Users键,但是我该如何将其与密码关联起来?再次感谢您。 - Alex
1
@Alex - 我同意答案的质量。答案在Remus帖子的顶部CREATE INDEX idxUsersNamePassword on Users(Name,Password);,你可能会发现这个链接有用,作为寻找其他可能缺失的索引的起点 http://blogs.msdn.com/b/bartd/archive/2007/07/19/are-you-using-sql-s-missing-index-dmvs.aspx - Martin Smith
难以置信!我刚刚将Name列注册为IX唯一索引,然后它...奏效了!不再有死锁,一个也没有。我不知道该如何表达我的快乐之情。谢谢大家。我感觉自己也需要贡献力量,帮助其他人。很棒的是,有人愿意免费提供帮助,因为他们只是想帮助别人。 - Alex

1

你的问题与这里的情况有很多相似之处在 SQL Server 2005 中诊断死锁

(Linq to SQL,只读事务被读写事务死锁)

如果你使用的是 SQL2005 或更高版本,也许根据那个帖子中讨论的设置快照隔离可以解决问题。否则,请在帖子中更新并提供你所使用的具体版本信息。


1
在这种情况下(即您正在读取的数据类型和对该数据进行的更新的性质),我会在未提交读取隔离级别下运行用户查找查询。
或者,更复杂的更改。根据您发布的描述,我会考虑不在用户记录上维护视图计数。相反,我会记录Article的ViewCount,然后通过AuthorID的Articles.ViewCount总和来推导用户的总浏览量。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接