Sco*_*nJL 9 sql-server materialized-view locking
我有你的基本标题/细节表(想想订单和订单细节)。标题表有一个标识列作为聚集键,详细信息有一个标题 id 和一个行号列作为聚集键。header id 是一个不断增加的标识值,行号也是一个递增的值。
我试图在细节上添加一个索引视图来聚合数据,所以我们不必在代码中或通过触发器来做这件事,在现有系统中有它自己的一组并发问题。
在我们开始对其进行负载测试之前,一切看起来和工作正常。预计表中将添加约 1500 条详细信息/秒(90,000 条/分钟)。
当一行插入到明细表时,索引视图也会更新。在插入过程中,索引视图上出现共享范围锁 (RangeS-U)。取的范围是下一个键的当前键,类似于在可序列化隔离级别下获取锁的方式。连接是在已提交读下设置的。当表中不存在“下一个”键时,似乎会出现瓶颈。在这种情况下,共享锁被用于 'infinity(ffffffff)' 键。
这基本上描述了我看到的行为,但没有提供任何解决方法。 https://www.brentozar.com/archive/2018/09/locks-taken-during-indexed-view-modifications/
在上述负载下,服务器无法跟上插入的速度,并且事情开始备份得非常快。600 个并发连接中有 500 个在给定时间被阻塞。对不断增加的键的聚合索引视图似乎无法满足我们的并发要求。
我们使用的是 SQL Server 2012 标准版,很快就会升级到 2019。
有什么方法可以改变索引视图上的这种锁定行为,或者这对我来说是徒劳的,在这种情况下,我需要走基于代码/触发器的聚合之路,还是我错过了什么?如果 2019 没有表现出对我有用的行为,因为数据库将在工作完成之前升级。
包含的脚本代表所涉及的表,但显然不是实际的表。使用它们可以重现该行为。
设置
if object_id(N'dbo.LockTest') is null
begin
create table dbo.LockTest
( LockTestID int not null primary key
, LockTestValue int null
);
insert into dbo.locktest values(1, 1), (2, 2), (3, 3), (7, 7), (8, 8);
end;
if object_id(N'dbo.LockTestDetails') is null
begin
create table dbo.LockTestDetails
( LockTestID int not null
, LineNumber int not null
, Val int not null
, PRIMARY KEY(LockTestID, LineNumber)
, foreign key(LockTestID) references dbo.LockTest(LockTestID)
);
insert into dbo.LockTestDetails values(2, 1, 5), (2, 2, 4);
end
if object_id(N'dbo.LockTestTotals') is null
begin
exec sp_executesql N'
CREATE VIEW dbo.LockTestTotals with schemabinding as
SELECT d.LockTestID, Lines = COUNT_BIG(*), Val = SUM(Val)
FROM dbo.LockTestDetails d
GROUP BY d.LockTestID';
exec sp_executesql N'
create unique clustered index PK_LockTestTotals on dbo.LockTestTotals(LockTestID)';
end
Run Code Online (Sandbox Code Playgroud)
示例 1
-- run in session 1.
-- range lock taken from 1 to the next key, 2.
begin transaction
insert into dbo.LockTestDetails values(1, 1, 1);
waitfor delay '00:00:20';
rollback
-- run in session 2
-- record is inserted. not blocked by session 1 range lock.
-- range lock taken from 7 to next key, 'infinity(ffffffff)'
-- no other details can be added with an id higher than 7.
begin transaction
insert into dbo.LockTestDetails values(7, 1, 1);
waitfor delay '00:00:20';
rollback
Run Code Online (Sandbox Code Playgroud)
示例 2
-- run in session 1.
-- range lock taken from 7 to next key, 'infinity(ffffffff)'
-- no other details can be added with an id higher than 7.
begin transaction
insert into dbo.LockTestDetails values(7, 1, 1);
waitfor delay '00:00:20';
rollback
-- run in session 2
-- record is blocked by session 1 range lock.
begin transaction
insert into dbo.LockTestDetails values(8, 1, 1);
waitfor delay '00:00:20';
rollback
Run Code Online (Sandbox Code Playgroud)
查看锁的脚本
declare @session int = null;
select
l.request_session_id
, l.resource_type
, resource_description = rtrim(l.resource_description)
, [object_name] = CASE
WHEN resource_type = 'OBJECT'
THEN OBJECT_SCHEMA_NAME(l.resource_associated_entity_id) + '.' + OBJECT_NAME(l.resource_associated_entity_id)
ELSE OBJECT_SCHEMA_NAME(p.[OBJECT_ID]) + '.' + OBJECT_NAME(p.[object_id])
END
, index_name = i.[name]
, l.request_mode
, l.request_status
, l.resource_subtype
, l.resource_associated_entity_id
from sys.dm_tran_locks l
left join sys.partitions p
ON p.hobt_id = l.resource_associated_entity_id
LEFT JOIN sys.indexes i
ON i.[OBJECT_ID] = p.[OBJECT_ID]
AND i.index_id = p.index_id
where resource_database_id = db_id()
and request_session_id between isnull(@session, 0) and isnull(@session, 5000)
and request_session_id <> @@spid
order by
[object_name]
, CASE
WHEN i.[name] is null then 0
WHEN LEFT(i.[name], 2) = 'PK' THEN 1
WHEN LEFT(i.[name], 2) = 'UK' THEN 2
ELSE 3 END
, index_name
, case resource_type
when 'DATABASE' then 0
when 'OBJECT' then 1
when 'PAGE' then 2
when 'KEY' then 3
when 'RID' then 4
else 99 end
, resource_description
, request_session_id;
Run Code Online (Sandbox Code Playgroud)
Pau*_*ite 10
对此你无能为力。SQL Server 自动采取措施确保索引视图始终与基表保持同步。
在读取索引视图以查看与更改的键关联的数据是否存在时,SQL Server 需要确保在视图维护完成之前数据不会更改。这包括密钥不存在的情况 -在插入之前它必须继续不存在。引擎通过在serializable隔离下访问索引视图来满足此要求。无论会话的当前隔离级别如何,都会发生此本地隔离升级。
出于兴趣,添加到索引视图读取的提示是:
UPDLOCK SERIALIZABLE DETECT-SNAPSHOT-CONFLICT
Run Code Online (Sandbox Code Playgroud)
该DETECT-SNAPSHOT-CONFLICT提示指示 SQL Server 在快照隔离下检查写入冲突。
在您的示例中,引擎还会向父表的读取添加提示以验证外键关系:
READ-COMMITTEDLOCK FORCEDINDEX DETECT-SNAPSHOT-CONFLICT
Run Code Online (Sandbox Code Playgroud)
该READ-COMMITTEDLOCK提示可确保在已提交读的快照隔离下运行时获取共享锁。
这些提示是正确性所必需的,不能被禁用。
您可能会考虑使聚集索引下降而不是上升,但这会引入额外的问题(对于上升插入),并且只会将争用点从结构的一端移动到另一端。
如果您尝试使用触发器或数据库外的代码编写相同的逻辑,您最终将错过边缘情况(导致不准确的摘要)或使用与 SQL Server 几乎相同的提示。众所周知,这种逻辑第一次就很难正确,并且需要在高并发下进行大量测试才能验证。另一方面,在某些情况下,粗略的总数可能就足够了。
如果您可以容忍一些延迟,您可以批量插入并在单个会话/线程上将它们批量应用于索引视图。例如,通过将插入的行保存在临时区域中,然后不时地从一个插入语句中更新基表。此处“批量”的含义不必特别大,只需足以轻松应对预期的峰值工作负载即可。这将使错误报告复杂化。
从根本上说,索引视图通常不太适合非常快速的基表更新,尤其是范围结束插入。
| 归档时间: |
|
| 查看次数: |
561 次 |
| 最近记录: |