我有一个存储过程,可以执行TableB
到TableA
的连接:
SELECT <--- Nested <--- TableA
Loop <--
|
---TableB
同时,在一个事务中,行被插入TableA
,然后插入TableB
。
这种情况偶尔会导致死锁,因为存储过程选择从 TableB 抓取行,而插入会向 TableA 添加行,然后每个人都希望另一个让去另一张桌子:
INSERT SELECT
========= ========
Lock A Lock B
Insert A Select B
Want B Want A
....deadlock...
逻辑要求INSERT
首先将行添加到 A ,然后再添加到 B ,而我个人并不关心SQL Server的顺序执行连接 - 只要它加入。
修复死锁的一般建议是确保每个人以相同的顺序访问资源。但在这种情况下,SQL Server的优化器告诉我相反的顺序是“更好”。我可以强制执行另一个连接顺序,并且查询效果更差。
但我应该吗?
我是否应该使用我希望它使用的连接顺序来覆盖优化器,现在和永远?
或者我应该捕获错误原生错误1205 ,然后重新提交select语句?
问题不在于当我覆盖优化器并且它做非理想的事情时查询可能会执行多么糟糕。问题是:自动重试是否更好,而不是运行更糟糕的查询?
答案 0 :(得分:9)
自动重试死锁是否更好。原因是你可以修复这个死锁,以后再打另一个。如果表的大小发生更改,服务器硬件规格发生更改,并且服务器上的负载发生更改,则SQL发行版之间的行为可能会发生变化。如果死锁是频繁的,你应该采取积极的步骤消除它(索引通常是答案),但对于罕见的死锁(比如每10分钟左右),在应用程序中重试可以掩盖死锁。您可以重试读取或写入,因为写入当然是由正确的begin transaction / commit事务包围,以使所有写入操作保持原子状态,从而能够在没有问题的情况下重试它们。
另一个需要考虑的途径是开启read committed snapshot。启用此选项后,SELECT将不会执行任何锁定,但会产生一致的读取。
答案 1 :(得分:5)
为了避免死锁,最常见的建议之一是“以相同的顺序获取锁”或“以相同的顺序访问对象”。显然,这是完全合理的,但它总是可行的吗?它总是可能吗?当我无法遵循这个建议时,我会遇到案件。
如果我将一个对象存储在一个父表和一个或多个子表中,我根本不能遵循这个建议。插入时,我需要先插入我的父行。删除时,我必须按相反的顺序进行。
如果我在一个表中使用触及多个表或多行的命令,那么通常我无法控制获取哪个顺序锁(假设我没有使用提示)。
因此,在许多情况下,尝试以相同的顺序获取锁定并不能防止所有死锁。因此,无论如何我们需要某种处理死锁 - 我们不能假设我们可以完全消除它们。 当然,除非我们使用Service Broker或sp_getapplock序列化所有访问。
当我们在死锁之后重试时,我们很可能会覆盖其他进程的更改。我们需要意识到,其他人很可能修改了我们要修改的数据。特别是如果所有读者都在快照隔离下运行,那么读者就不会参与死锁,这意味着死锁中涉及的所有各方都是编写者,修改过或试图修改相同的数据。如果我们只是捕获异常并自动重试,我们可以覆盖其他人的更改。
这称为丢失更新,这通常是错误的。通常,在死锁之后做正确的事情是在更高级别上重试 - 重新选择数据并决定是否以最初保存决策的方式进行保存。
例如,如果用户按下了“保存”按钮并且选择了保存事务作为死锁牺牲品,那么在死锁之后重新显示屏幕上的数据可能是个好主意。
答案 2 :(得分:2)
陷阱和重新运行可以工作,但你确定SELECT始终是死锁受害者吗?如果插件是死锁受害者,则必须更加小心重试。
在这种情况下,我认为最简单的解决方案是选择NOLOCK或READUNCOMMITTED(同样的东西)。人们对脏读取有合理的担忧,但是我们已经多次运行NOLOCK以获得更高的并发性并且从未出现过问题。
我还会对锁语义进行更多研究。例如,我相信如果您将事务隔离级别设置为快照(需要2005或更高版本),您的问题就会消失。