Sak*_*o73 24 sql-server deadlock sql-server-2008-r2 merge
我有以下过程(SQL Server 2008 R2):
create procedure usp_SaveCompanyUserData
@companyId bigint,
@userId bigint,
@dataTable tt_CoUserdata readonly
as
begin
set nocount, xact_abort on;
merge CompanyUser with (holdlock) as r
using (
select
@companyId as CompanyId,
@userId as UserId,
MyKey,
MyValue
from @dataTable) as newData
on r.CompanyId = newData.CompanyId
and r.UserId = newData.UserId
and r.MyKey = newData.MyKey
when not matched then
insert (CompanyId, UserId, MyKey, MyValue) values
(@companyId, @userId, newData.MyKey, newData.MyValue);
end;
Run Code Online (Sandbox Code Playgroud)
CompanyId、UserId、MyKey 构成目标表的组合键。CompanyId 是父表的外键。此外,还有一个非聚集索引CompanyId asc, UserId asc。
它是从许多不同的线程调用的,我一直在调用相同语句的不同进程之间出现死锁。我的理解是“with (holdlock)”对于防止插入/更新竞争条件错误是必要的。
我假设两个不同的线程在验证约束时以不同的顺序锁定行(或页面),因此是死锁。
这是一个正确的假设吗?
解决这种情况的最佳方法是什么(即没有死锁,对多线程性能的影响最小)?
(如果您在新选项卡中查看图像,则它是可读的。抱歉尺寸较小。)
Pau*_*ite 35
如果表变量只保存一个值,就不会有问题。对于多行,出现死锁的新可能性。假设两个并发进程(A 和 B)使用包含同一公司的 (1, 2) 和 (2, 1) 的表变量运行。
进程 A 读取目的地,未找到任何行,并插入值“1”。它持有值“1”的排他行锁。进程 B 读取目标,未找到任何行,并插入值“2”。它持有值“2”的排他行锁。
现在进程 A 需要处理第 2 行,进程 B 需要处理第 1 行。两个进程都无法取得进展,因为它需要一个与另一个进程持有的排他锁不兼容的锁。
为了避免多行的死锁,每次都需要以相同的顺序处理(和访问表)行。问题中显示的执行计划中的表变量是堆,因此行没有内在顺序(它们很可能按插入顺序读取,尽管不能保证):

缺乏一致的行处理顺序直接导致死锁机会。第二个考虑因素是缺少密钥唯一性保证意味着必须使用 Table Spool 来提供正确的万圣节保护。假脱机是一个急切假脱机,这意味着所有行在被读回和为插入操作员重放之前都被写入tempdb工作表。
重新定义TYPE表变量的 以包含聚集的PRIMARY KEY:
DROP TYPE dbo.CoUserData;
CREATE TYPE dbo.CoUserData
AS TABLE
(
MyKey integer NOT NULL PRIMARY KEY CLUSTERED,
MyValue integer NOT NULL
);
Run Code Online (Sandbox Code Playgroud)
执行计划现在显示了对聚集索引的扫描,唯一性保证意味着优化器能够安全地删除 Table Spool:

在MERGE128 个线程上对语句进行5000 次迭代的测试中,聚簇表变量没有发生死锁。我要强调的是,这只是基于观察;聚簇表变量也可以(技术上)以各种顺序生成其行,但是一致顺序的机会大大增加。当然,需要针对每个新的累积更新、Service Pack 或 SQL Server 的新版本重新测试观察到的行为。
如果无法更改表变量定义,还有另一种选择:
MERGE dbo.CompanyUser AS R
USING
(SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
R.CompanyId = @CompanyID
AND R.UserID = @UserID
AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN
INSERT
(CompanyID, UserID, MyKey, MyValue)
VALUES
(@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);
Run Code Online (Sandbox Code Playgroud)
这也以引入显式排序为代价来消除假脱机(和行顺序一致性):

该计划使用相同的测试也没有产生死锁。复制脚本如下:
CREATE TYPE dbo.CoUserData
AS TABLE
(
MyKey integer NOT NULL /* PRIMARY KEY */,
MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
CompanyID integer NOT NULL
CONSTRAINT PK_Company
PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
CompanyID integer NOT NULL,
UserID integer NOT NULL,
MyKey integer NOT NULL,
MyValue integer NOT NULL
CONSTRAINT PK_CompanyUser
PRIMARY KEY CLUSTERED
(CompanyID, UserID, MyKey),
FOREIGN KEY (CompanyID)
REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE
@DataTable AS dbo.CoUserData,
@CompanyID integer = 1,
@UserID integer = 1;
INSERT @DataTable
SELECT TOP (10)
V.MyKey,
V.MyValue
FROM
(
VALUES
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8),
(9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();
BEGIN TRANSACTION;
-- Test MERGE statement here
ROLLBACK TRANSACTION;
Run Code Online (Sandbox Code Playgroud)
RBa*_*ung 12
好吧,在看了几遍之后,我认为你的基本假设是正确的。这里可能发生的是:
MERGE 的 MATCH 部分检查索引是否匹配,并在运行时读取锁定这些行/页面。
当它有一个没有匹配的行时,它会先尝试插入新的索引行,这样它就会请求一个行/页写锁......
但是如果另一个用户也进入了同一行/页面上的第 1 步,那么第一个用户将被阻止更新,并且......
如果第二个用户也需要在同一页面上插入,那么他们就会陷入僵局。
AFAIK,只有一种(简单的)方法可以 100% 确定您不会在此过程中陷入僵局,那就是向 MERGE 添加 TABLOCKX 提示,但这可能会对性能产生非常不利的影响。
这可能是加入了TABLOCK提示,而不是将足以解决问题,而不必大对广告效果产生影响。
最后,您还可以尝试添加 PAGLOCK、XLOCK 或 PAGLOCK 和 XLOCK。同样,这可能会奏效,而且性能可能不会太糟糕。你得试试看。
我认为 SQL_Kiwi 提供了非常好的分析。如果你需要解决数据库中的问题,你应该按照他的建议去做。当然,每次升级、应用服务包或添加/更改索引或索引视图时,您都需要重新测试它是否仍然适用于您。
还有其他三种选择:
您可以序列化您的插入,以便它们不会发生冲突:您可以在事务开始时调用 sp_getapplock 并在执行 MERGE 之前获取排他锁。当然,您仍然需要对其进行压力测试。
您可以让一个线程处理所有插入,以便您的应用服务器处理并发。
您可以在死锁后自动重试 - 如果并发性很高,这可能是最慢的方法。
无论哪种方式,只有您才能确定您的解决方案对性能的影响。
通常,我们的系统中根本没有死锁,尽管我们确实有很多可能发生死锁。2011 年,我们在一次部署中犯了一个错误,在几个小时内发生了六个死锁,所有情况都遵循相同的场景。我很快就解决了这个问题,这就是今年所有的僵局。
我们主要在我们的系统中使用方法 1。它对我们非常有效。
| 归档时间: |
|
| 查看次数: |
22525 次 |
| 最近记录: |