8

我有一个执行TableBto连接的存储过程TableA

 SELECT <--- Nested <--- TableA
             Loop   <--
                      |
                      ---TableB

同时,在事务中,将行插入到TableA中,然后再插入到 中TableB

这种情况偶尔会导致死锁,因为存储过程 select 从TableB抓取行,而 insert 将行添加到TableA,然后每个人都希望对方放手另一个表:

INSERT     SELECT
=========  ========
Lock A     Lock B
Insert A   Select B
Want B     Want A
....deadlock...

逻辑要求INSERT首先将行添加到A,然后添加到B,而我个人并不关心 SQL Server 执行连接的顺序 - 只要它连接即可。

修复死锁的常见建议是确保每个人都以相同的顺序访问资源。但在这种情况下,SQL Server 的优化器告诉我相反的顺序是“更好”。我可以强制执行另一个连接顺序,并且执行更差的查询。

但我应该吗?

我是否应该现在和永远使用我希望它使用的连接顺序来覆盖优化器?

或者我应该只捕获错误本机错误 1205,然后重新提交选择语句?

问题不是当我覆盖优化器并让它做一些非最优的事情时查询可能会执行多糟糕。问题是:自动重试比运行更糟糕的查询更好吗?

4

3 回答 3

9

自动重试死锁是否更好。原因是您可能会修复这个死锁,但稍后会遇到另一个死锁。如果表的大小发生变化,服务器硬件规格发生变化,甚至服务器上的负载发生变化,则行为可能会在 SQL 版本之间发生变化。如果死锁很频繁,你应该采取积极的措施来消除它(索引通常是答案),但是对于罕见的死锁(比如每 10 分钟左右),在应用程序中重试可以掩盖死锁。您可以重试读取写入,因为写入当然被正确的开始事务/提交事务包围,以保持所有写入操作原子,因此能够重试它们而不会出现问题。

另一个需要考虑的途径是打开已提交的读取快照。启用此功能后,SELECT 将根本不使用任何锁,但会产生一致的读取。

于 2010-03-04T22:20:10.963 回答
5

为了避免死锁,最常见的建议之一是“以相同的顺序获取锁”或“以相同的顺序访问对象”。显然,这完全有道理,但它总是可行的吗?总是有可能吗?当我无法遵循此建议时,我不断遇到案例。

如果我将一个对象存储在一个父表和一个或多个子表中,我根本无法遵循这个建议。插入时,我需要先插入我的父行。删除时,我必须以相反的顺序进行。

如果我使用涉及多个表或一个表中的多行的命令,那么通常我无法控制获取锁的顺序(假设我没有使用提示)。

因此,在许多情况下,尝试以相同顺序获取锁并不能防止所有死锁。所以,无论如何,我们都需要某种处理死锁的方法——我们不能假设我们可以消除所有死锁。当然,除非我们使用 Service Broker 或 sp_getapplock 序列化所有访问。

当我们在死锁后重试时,我们很可能会覆盖其他进程的更改。我们需要意识到,很可能其他人修改了我们打算修改的数据。特别是如果所有读取器都运行在快照隔离下,那么读取器就不会被卷入死锁,这意味着死锁的所有参与方都是写入器,被修改或试图修改相同的数据。如果我们只是捕获异常并自动重试,我们可以覆盖其他人的更改。

这称为丢失更新,这通常是错误的。通常,死锁后正确的做法是在更高级别上重试 - 重新选择数据并决定是否以与最初决定保存相同的方式保存。

例如,如果用户按下保存按钮并且保存事务被选为死锁牺牲品,那么在死锁之后重新在屏幕上显示数据可能是个好主意。

于 2010-03-05T01:32:42.467 回答
2

捕获和重新运行可以工作,但您确定 SELECT 始终是死锁的牺牲品吗?如果插入是死锁的牺牲品,那么重试时您必须更加小心。

我认为,在这种情况下,最简单的解决方案是 NOLOCK 或 READUNCOMMITTED (同样的事情)你的选择。人们对脏读有合理的担忧,但多年来我们一直在到处运行 NOLOCK 以获得更高的并发性,并且从未遇到过问题。

我还会对锁语义做更多的研究。例如,我相信如果您将事务隔离级别设置为快照(需要 2005 或更高版本),您的问题就会消失。

于 2010-03-04T21:57:14.603 回答