32

我在运行在 MS SQL Server 2005 之上的 .NET 2.0 webapp 上遇到了非常罕见但令人讨厌的 SQL 死锁。过去,我们一直以非常经验的方式处理 SQL 死锁 - 基本上是调整查询直到它工作。

然而,我发现这种方法非常不令人满意:耗时且不可靠。我非常希望遵循确定性查询模式,这些模式将通过设计确保不会遇到 SQL 死锁 - 永远。

例如,在 C# 多线程编程中,一个简单的设计规则(如锁)必须按照其字典顺序来确保不会发生死锁。

是否有任何 SQL 编码模式可以保证是防死锁的?

4

10 回答 10

22

编写防死锁代码真的很难。即使您以相同的顺序访问表,您仍然可能会遇到死锁 [1]。我在我的博客上写了一篇文章,详细阐述了一些有助于避免和解决死锁情况的方法。

如果您想确保两个语句/事务永远不会死锁,您可以通过使用sp_lock系统存储过程观察每个语句使用哪些锁来实现它。为此,您必须要么非常快,要么使用带有锁定提示的打开事务。


笔记:

  1. 任何一次需要多个锁的 SELECT 语句都可能因智能设计的事务而死锁,该事务以相反的顺序获取锁。
于 2008-09-21T21:41:25.333 回答
13

在一般情况下,零死锁基本上是一个代价高昂的问题,因为您必须知道要为每个正在运行的事务(包括 SELECT)读取和修改的所有表/对象。一般哲学称为有序严格两阶段锁定(不要与两阶段提交混淆)(http://en.wikipedia.org/wiki/Two_phase_locking;即使 2PL 也不能保证没有死锁)

很少有 DBMS 真正实施严格的 2PL,因为这样的事情会导致巨大的性能损失(没有免费的午餐),而您的所有事务都在等待执行甚至是简单的 SELECT 语句。

无论如何,如果这是您真正感兴趣的东西,请查看SET ISOLATION LEVELSQL Server。您可以根据需要进行调整。http://en.wikipedia.org/wiki/Isolation_level

有关更多信息,请参阅关于 Serializability 的维基百科:http ://en.wikipedia.org/wiki/Serializability

也就是说——一个很好的类比就像源代码修订:尽早并经常检查。让您的事务保持小(以 SQL 语句数、修改的行数为单位)和快速(挂钟时间有助于避免与其他人发生冲突)。在一个事务中做很多事情可能会很好而且很整洁——总的来说我同意这个哲学——但是如果你遇到很多死锁,你可以把事务分解成更小的事务,然后在您移动时检查他们在应用程序中的状态。TRAN 1 - OK Y/N?如果是,发送 TRAN 2 - OK Y/N? 等等等等

顺便说一句,在我作为一名 DBA 和开发人员(测量数千个并发用户的多用户 DB 应用程序)的多年中,我从未发现死锁是一个如此严重的问题,以至于我需要特别注意它(或改变隔离)水平无忧无虑等)。

于 2008-09-22T02:34:06.127 回答
3

对于这个问题,在实践中没有神奇的通用解决方案。您可以将并发推送到应用程序,但这可能非常复杂,特别是如果您需要与在单独的内存空间中运行的其他程序进行协调。

减少死锁机会的一般答案:

  1. 基本查询优化(适当的索引使用)热点避免设计,尽可能短的时间保持事务......等。

  2. 尽可能设置合理的查询超时,以便如果发生死锁,它会在超时期限到期后自行清除。

  3. MSSQL 中的死锁通常是由于其默认的读取并发模型,因此不依赖它非常重要 - 在所有设计中假设 Oracle 风格的 MVCC。使用快照隔离,或者如果可能,使用 READ UNCOMMITED 隔离级别。

于 2009-02-08T05:36:22.813 回答
2

我相信以下有用的读/写模式是死锁证明给定一些限制:

约束:

  1. 一张桌子
  2. 索引或 PK 用于读/写,因此引擎不会求助于表锁。
  3. 可以使用单个 SQL where 子句读取一批记录。
  4. 使用 SQL Server 术语。

写周期:

  1. 单个“已提交读”事务中的所有写入。
  2. 事务中的第一次更新是针对每个更新组中的特定的、始终存在的记录。
  3. 然后可以以任何顺序写入多个记录。(它们受到写入第一条记录的“保护”)。

阅读周期:

  1. 默认的读提交事务级别
  2. 无交易
  3. 将记录作为单个选择语句读取。

好处:

  1. 二级写周期在写第一条记录时被阻塞,直到第一个写事务完全完成。
  2. 在写入提交之间以原子方式阻止/排队/执行读取。
  3. 无需借助“可序列化”即可实现事务级别的一致性。

我也需要这个工作,所以请评论/更正!!

于 2011-11-14T19:57:28.740 回答
1

正如您所说,始终以相同的顺序访问表是避免死锁的好方法。此外,尽可能缩短您的交易时间。

另一个很酷的技巧是尽可能将 2 个 sql 语句组合在一起。单个语句始终是事务性的。例如使用“UPDATE ... SELECT”或“INSERT ... SELECT”,使用“@@ERROR”和“@@ROWCOUNT”而不是“SELECT COUNT”或“IF (EXISTS ...)”

最后,确保您的调用代码可以通过以可配置的次数重新发布查询来处理死锁。有时它只是发生,这是正常行为,您的应用程序必须能够处理它。

于 2008-09-21T18:57:20.733 回答
1

如果您对应用程序有足够的设计控制权,请将更新/插入限制为特定的存储过程,并从应用程序使用的数据库角色中删除更新/插入权限(仅明确允许通过这些存储过程进行更新)。

将您的数据库连接隔离到应用程序中的特定类(每个连接都必须来自此类)并指定“仅查询”连接将隔离级别设置为“脏读”......相当于每个连接上的(nolock) .

这样,您就可以隔离可能导致锁定的活动(针对特定的存储过程)并从“锁定循环”中取出“简单读取”。

于 2008-09-22T02:49:50.973 回答
1

除了一致的锁获取顺序之外 - 另一条路径是显式使用锁定和隔离提示来减少在读取期间无意获取锁(例如共享意图)所浪费的时间/资源。

于 2008-09-22T05:05:06.703 回答
1

没有人提到的(令人惊讶的)是,在 SQL 服务器方面,许多锁定问题可以通过数据库查询工作负载的正确覆盖索引​​集来消除。为什么?因为它可以大大减少对表的聚集索引(假设它不是堆)的书签查找次数,从而减少争用和锁定。

于 2008-11-03T01:23:25.140 回答
0

快速回答是否定的,没有保证技术。

如果它具有任何非平凡的吞吐量,我看不出如何将任何应用程序死锁证明作为设计原则。如果您以相同的顺序预先锁定进程中可能需要的所有资源,即使您最终不需要它们,您也会面临第二个进程等待获取它需要的第一个锁的代价更高的问题,并且您的可用性受到影响。随着系统中资源数量的增长,即使是微不足道的进程也必须以相同的顺序锁定它们以防止死锁。

与大多数性能和可用性问题一样,解决 SQL 死锁问题的最佳方法是查看分析器中的工作负载并了解行为。

于 2008-09-22T03:43:13.280 回答
0

不是直接回答您的问题,而是值得深思:

http://en.wikipedia.org/wiki/Dining_philosophers_problem

“餐饮哲学家问题”是一个古老的思想实验,用于检验死锁问题。阅读它可能会帮助您找到针对您的特定情况的解决方案。

于 2008-09-22T05:55:18.733 回答