SQL Server 2000 死锁分析
SQL Server 2000 中的死锁分析。
引言
SQL Server 死锁并不少见。大多数复杂的系统都会遇到某种死锁。几乎不可能完全避免死锁,这就是为什么 SQL Server 内置了死锁检测功能。SQL Server 通过终止参与死锁的事务之一来解决死锁,通常被牺牲的是资源消耗较少的事务。过多的死锁会严重影响应用程序的性能和可伸缩性。
什么是死锁
死锁是一种特殊的阻塞场景,其中两个或多个线程相互阻塞,导致任何一个都无法继续执行。考虑两个事务 T1 和 T2 使用资源 R1 和 R2。T1 持有 R1 的排他锁,T2 持有 R2 的排他锁。在执行的任何时间点,如果 T1 需要 R2 的排他锁而 T2 需要 R1 的排他锁,那么 T1 和 T2 都将发生死锁。网上有很多文章解释死锁以及如何处理它们。在这里,我将尝试解释如何追踪实时死锁及其解决方案;当然,一旦我们知道原因,解决方案就很简单。
问题定义
所讨论的应用程序是一个简单的 Remoting 服务器,为大约 300 个并发用户提供服务。该应用程序编写良好,并遵循大多数最佳实践。启用 SQL Server 跟踪后,发现大量的死锁和由此产生的事务失败。要打开标志 1204,请使用 DBCC TRACEON (1222, -1)。SQL Server 2005 引入了经过尝试和测试的 T1024 的新版本和改进版本。如果您使用的是 SQL Server 2005,请使用 T1222 标志。下面是 T1024 标志打印的死锁信息(为清晰起见已截断)
// Any Deadlock encountered .... Printing deadlock information
2007-09-13 16:54:47.15 spid4
2007-09-13 16:54:47.15 spid4 Wait-for graph
2007-09-13 16:54:47.15 spid4
2007-09-13 16:54:47.15 spid4 Node:1
2007-09-13 16:54:47.15 spid4 KEY: 7:645577338:1 (12014f0bec4f)
CleanCnt:2 Mode: Range-S-S Flags: 0x0
2007-09-13 16:54:47.15 spid4 Grant List::
2007-09-13 16:54:47.15 spid4 Owner:0x1a3cc3c0 Mode: Range-S-S Flg:0x0
Ref:2 Life:02000000 SPID:56 ECID:0
2007-09-13 16:54:47.15 spid4 SPID: 56 ECID: 0 Statement
Type: INSERT Line #: 1
2007-09-13 16:54:47.15 spid4 Input Buf: Language
Event: Insert from t_cash_folder
(ADDED_DT ,
2007-09-13 16:54:47.15 spid4 Requested By:
2007-09-13 16:54:47.15 spid4 ResType:LockOwner Stype:'OR'
Mode: Range-Insert-Null SPID:51
ECID:0 Ec:(0x1a4a9570)
Value:0x25af8760 Cost:(0/D0)
2007-09-13 16:54:47.15 spid4
2007-09-13 16:54:47.15 spid4 Node:2
2007-09-13 16:54:47.15 spid4 KEY: 7:645577338:1 (12014f0bec4f)
CleanCnt:2 Mode: Range-S-S Flags: 0x0
2007-09-13 16:54:47.15 spid4 Grant List::
2007-09-13 16:54:47.15 spid4 Owner:0x1a3c4e80 Mode: Range-S-S
Flg:0x0 Ref:2 Life:02000000
SPID:51 ECID:0
2007-09-13 16:54:47.15 spid4 SPID: 51 ECID: 0 Statement
Type: INSERT Line #: 1
2007-09-13 16:54:47.15 spid4 Input Buf: Language
Event: Insert into t_cash_folder (...
2007-09-13 16:54:47.15 spid4 Requested By:
2007-09-13 16:54:47.15 spid4 ResType:LockOwner Stype:'OR'
Mode: Range-Insert-Null SPID:56 ECID:0
Ec:(0x1e4f5570)
Value:0x1a3c56e0 Cost:(0/D0)
2007-09-13 16:54:47.15 spid4 Victim Resource Owner:
2007-09-13 16:54:47.15 spid4 ResType:LockOwner Stype:'OR'
Mode: Range-Insert-Null SPID:56 ECID:0
Ec:(0x1e4f5570)
Value:0x1a3c56e0 Cost:(0/D0)
2007-09-13 16:54:52.15 spid4
//
分析 T1024 输出
我将尽量使分析简单化。死锁信息中的每个节点代表涉及的资源。在每个节点下,我们有三个部分:KEY、GrantList 和 Requested By。
KEY: 7:645577338:1 (12014f0bec4f) CleanCnt:2 Mode: Range-S-S Flags: 0x0
KEY 后面的 ID 是它在系统表(systables 和 sysindexes)中出现的资源 ID。神秘的 resourceID KEY 有三个部分:{KEY : DatabaseID : TableID: IndexType },其中 IndexType 是聚集索引为 1,非聚集索引大于 1。7:645577338:1 (12014f0bec4f)
代表数据库 7 中 ID 为 645577338 的表上的聚集索引(哈希值=12014f0bec4f)。
另一个需要注意的重要点是锁模式;KEY 上的 Mode 属性是为资源授予的最高锁模式。KEY 的其他属性可以忽略。
GrantList
GrantList 顾名思义,列出了所有已获得对所讨论资源某种类型锁的进程。它列出了持有锁的 SPID、锁模式以及正在执行的 SQL。
Owner:0x1a3cc3c0 Mode: Range-S-S Flg:0x0 Ref:2 Life:02000000 SPID:56 ECID:0 spid4
SPID: 56 ECID: 0 Statement Type: INSERT Line #: 1 spid4 Input Buf: Language
Event: Insert into t_cash_folder (ADDED_DT
SPID 56 持有 ID 为 12014f0bec4f 的资源上的 Range-S-S 锁,并尝试执行 INSERT
。现在的问题是,为什么 INSERT
语句持有 Range-S-S 锁,因为我们知道 Range-S-S 是一个串行范围扫描,通常在*串行*隔离模式下由 SELECT
触发。还要记住,INSERT
不是当前正在执行的语句,而是最初请求锁的语句。因此,可以推断在*串行*隔离上下文中执行了 SELECT
语句,该语句需要 RangeS-S 锁。
Requested By
此部分列出了等待获取同一资源锁的 SPID。SPID 51 正在等待获取该资源的 Range-Insert-Null 锁。Range-Insert-Null 锁是在实际插入之前测试可插入性所必需的。
Requested By: 2007-09-13 16:54:47.15 spid4 ResType:LockOwner Stype:'OR'
Mode: Range-Insert-Null SPID:51 ECID:0 Ec :(0x1a4a9570) Value:0x25af8760 Cost:(0/D0)
现在我们知道了发生了什么。SPID 56 持有 Range-S-S 锁,并等待同一资源的 Range-Insert-Null,而 SPID 51 也持有 Range-S-S 锁(Range S-S 锁本身是兼容的),并等待 Range-Insert-Null。现在导致死锁的序列是
- SPID 56 在聚集索引 B+ 树分支上获取了 Range-S-S 锁。
- SPID 51 在同一聚集索引 B+ 树分支上获取了 Range-S-S 锁。
- SPID 56 需要该分支上的 Range-Insert-Null 锁,并正在等待 SPID 51 释放其 RangeS-S 锁。
- SPID 51 需要该分支上的 Range-Insert-Null 锁,并正在等待 SPID 56 释放其 RangeS-S 锁。
解决方案
两个 SPID 持有的 Range-S-S 锁表明应用程序正在使用*串行*隔离模式,因为在*串行*隔离模式下使用 SELECT
时会使用此锁定模式。*串行*隔离是可能的最高隔离模式,可消除幻读并保证可重复读取。可重复读取的保证要求 RangeS-S 锁在事务范围内被持有。
如果应用程序实际上不需要*串行*隔离模式,您可以尝试更改隔离模式。如果您倾向于使用 System.Transaction
而不是 ADO.NET 事务,请记住前者默认使用*串行*隔离模式。另一个想法是考虑将 Select
语句移出事务。这将消除对 RangeS-S 锁的需求,当然也消除了死锁。
结论
SQL Server 设计用于处理死锁,并且有一个专门的代理会不时唤醒以查找死锁并解决它们。您的代码应编写为处理可能的死锁,并在需要时重新提交同一事务。除了在代码中处理死锁外,简单的代码重排和应用程序之间一致的数据库访问还可以避免许多死锁。即使您无法完全避免死锁,通过遵循一些最佳实践也可以将其降至最低。请记住,死锁是应用程序可伸缩性的杀手。