已经阅读了所有其他死锁问题,但是似乎这些问题通常是特定于查询的,因此无法通过发布的答案解决我的特定问题。
我有一个Python脚本对此数据库运行多个并发UPDATE,并且当线程数设置得太高时,我会陷入僵局。
下面的查询正在发生死锁,我不确定应该使用哪种“表提示”组合,或者是否有更好的方法来执行此UPDATE
语句。
这是我的查询(为简便起见,已修改名称):
BEGIN TRAN
IF EXISTS (SELECT BlahID FROM MyTable WITH (NOLOCK) WHERE BlahID = ?)
BEGIN
UPDATE MyTable SET
Foo = ?,
Bar = 1
WHERE BlahID = ?
END
ELSE
BEGIN
INSERT INTO MyTable (Foo, Bar)
VALUES (1, ?,)
END
COMMIT TRAN
答案 0 :(得分:1)
您不需要IF
来检查记录是否已经存在。 UPDATE语句中的WHERE
子句可以做到这一点。您需要做的是确保在插入新记录之前不存在记录,例如:
UPDATE MyTable
SET
Foo = @foo,
Bar = 1
WHERE BlahID = @id;
INSERT MyTable (Bar,Foo)
values (1,@foo)
where not exists (select BlahID
from MyTable
where BlahID=@id)
如果可能,请使用命名参数,因此您只需传递2个参数(而不是4个),就可以使顺序混淆。
您可以将两个语句包装在一个事务中,但确保对BlahID进行索引。这将允许服务器仅锁定一行以进行更新。没有索引,服务器将不得不扫描并锁定更多数据以确保一致性。
这也避免了插入重复的条目。无论您进行多少次锁定,如果您使用IF
子句,两次并发尝试使用相同的不存在的ID都会导致两次插入,因为两个查询都会发现该行丢失,两者都会尝试无条件插入。
另一种选择是使用MERGE,尽管在这种情况下它不能很好地执行。从MERGE documentation
仅基于另一个表的行更新一个表时,可以使用基本的INSERT,UPDATE和DELETE语句来提高性能和可伸缩性。例如:
INSERT tbl_A (col, col2)
SELECT col, col2
FROM tbl_B
WHERE NOT EXISTS (SELECT col FROM tbl_A A2 WHERE A2.col = tbl_B.col);
当前情况更简单,只涉及一张表:
INSERT MyTable (Bar,Foo)
VALUES (1,@foo)
WHERE NOT EXISTS (SELECT BlahID FROM MyTable WHERE BlahID=@id);
为什么会陷入僵局?
服务器必须锁定行以确保事务可重复。选择时,服务器对检索或扫描的行采取SHARED(S)锁。这就是为什么拥有索引会导致更少锁定的原因-服务器可以立即找到它需要的行。这些共享锁将在事务期间保持不变。如果没有显式事务,则根据隔离模式,可以在连接期间保留共享锁。 REPEATABLE READ就是这种情况。
当您尝试更新一行时,服务器将尝试获取UPDATE锁。如果某行具有共享锁定服务器,则更新操作将被阻止。如果某个事务已经在行上拥有SHARED锁,它将尝试将其升级为UPGRADE锁。如果其他人在该行上具有S锁,则交易会被阻止。为了使读取内容可重复 ,服务器必须锁定其触摸的行。
如果服务器由于缺少索引而无法定位单行,情况会更糟。
NOLOCK并不意味着不使用任何锁,这意味着不尊重他人的锁。该操作仍将锁定,但会导致脏结果,重影或缺少更新。
在这种情况下,这就是引起脱脂的原因:
IF(SELECT)
并在行S1和S2上获得SHARED锁定。您可以在Locking in the Database Engine的SQL Server Transaction Locking and Row Versioning Guide部分中找到有关锁定,锁定类型,兼容性和范围的更多信息
快照隔离
您可以使用snapshot isolation level来避免读者和作者彼此阻塞,这与Oracle和PostgreSQL相似。在 this 情况下,这无济于事,因为您有一位作家阻止了另一位作家。
答案 1 :(得分:0)
您不希望多个会话针对相同的键值运行第一个SELECT。这就是导致僵局的原因。
这里的正确模式是:
BEGIN TRAN
IF EXISTS (SELECT BlahID FROM MyTable WITH (UPDLOCK,HOLDLOCK) WHERE BlahID = ?)
BEGIN
UPDATE MyTable SET
Foo = ?,
Bar = 1
WHERE BlahID = ?
END
ELSE
BEGIN
INSERT INTO MyTable (Foo, Bar)
VALUES (1, ?,)
END
COMMIT TRAN
如果该行存在,则SELECT锁定该行;如果该行不存在,则获取键范围上的更新范围锁。在这两种情况下,第二个会话都将阻止存在检查,直到第一个会话完成插入或更新。
如果您不使用锁提示(在SELECT,UPDATE,INSERT或MERGE中)读取,那么如果该行不存在,则不会进行任何锁,并且多个会话可以尝试INSERT。
>答案 2 :(得分:0)
我最终向“ BlahID”字段添加了“唯一约束”,因为它似乎在我的第一个UPDATE语句上执行了整个TABLE级别的锁定。添加此约束后,我相信它随后只能正确执行行级锁定,从而为我解决了死锁问题。
我还为自己的UPDATE报废了“ IF / ELSE”格式,只是这样做:
UPDATE MyTable SET
Foo = ?
WHERE BlahID = ?
IF @@ROWCOUNT=0
INSERT INTO MyTable (Foo)
VALUES (1)
我从“表提示”和锁定其他提供的答案中学到了很多东西,所以如果您是一个任性的Googler,则值得一读!