描述
在SQL Server中,内部闩锁体系结构可在SQL操作期间保护内存。通过页面上的读写操作,可以确保内存结构的一致性。从根本上讲,它具有两个类:缓冲区锁存器和非缓冲区锁存器,它们在SQL Engine中执行轻量级同步。
闩锁确保内存一致性,而锁确保逻辑事务一致性。当多个用户或应用程序同时访问同一数据时,锁定会阻止他们同时更改数据。锁由Microsoft SQL Server Compact数据库引擎在内部进行管理。用户执行DML操作时,将自动获取锁并在资源上释放锁。锁存器可确保包括索引和数据页在内的存储器结构上的存储器一致性。通常,SQL Server使用缓冲池和IO锁存器来处理同步原始方式的内存结构。当服务器上有多线程并发负载时,由于尝试获取不兼容的内存结构而导致闩锁,并且这样做会导致闩锁争用 可能会出现问题。
SQL Server闩锁的类型很多,包括缓冲区,非缓冲区和IO闩锁。有关闩锁的更多说明,请查看这篇文章 。此外,我将详细讨论热闩锁,以及如何识别和解决它们。
锁存模式和兼容性
基本上,锁存器是在5种不同的模式下获取的:KP(保留锁存器),SH(共享锁存器),UP(更新锁存器),EX(排他锁存器),DT(销毁锁存器)。下面总结一下闩锁模式及其兼容性。
KP(保持闩锁)
保持闩锁可确保引用的结构不会被破坏。
SH(共享锁存器)
需要共享锁存器才能读取页面数据结构。共享锁存器(SH)与更新(UP)或保持(KP)锁存器兼容,但与销毁锁存器(DT)不兼容。
UP(更新锁存器)
更新锁存器与Keep锁存器和共享锁存器兼容,但是没有人可以写入其参考结构。
EX(专用锁存器)
此锁存器阻止其他线程等待或从参考区域读取。
DT(销毁闩锁)
销毁该内容之前,已将其分配给引用结构的内容。
与锁类似,所有这些锁存模式都存在兼容或者不兼容。例如,当线程尝试获取可能的闩锁且模式不兼容时,则将其放入队列中以等待资源可用。下面为闩锁模式兼容性表。
锁存器 | KP | SH | UP | EX | DT |
KP | Y | Y | Y | Y | N |
SH | Y | Y | Y | N | N |
UP | Y | Y | N | N | N |
EX | Y | N | N | N | N |
DT | Y | N | N | N | N |
闩锁等待类型
对于并发负载,由于锁存模式不兼容,可能会导致页面争用。我们可以借助不同的SQL Server DMV的等待类型来找出这些争用问题。sys.dm_os_wait_stats,sys.dm_os_latch_stats,sys.dm_exec_query_stats等。
- 在DMV中以前缀PAGELATCH_ * 开头的缓冲区锁存器(BUF)。(例如PAGELATCH_EX,PAGELATCH_SH)
- 非缓冲锁存器(Non-BUF)在DMV中以前缀LATCH_ *开头。(例如LATCH_UP,LATCH_EX,LATCH_SH,LATCH_DT)
- IO闩锁带有前缀PAGEIOLATCH_ *。(例如PAGEIOLATCH_SH,PAGEIOLATCH_EX)
热锁(PAGELATCH_EX)
根据上述概述,由于这些锁存器争用而产生了不同的等待类型。在这些等待中,我将重点放在等待类型PAGELATCH_EX上。
此 等待类型表示当线程正在等待访问内存中的数据文件页面时,因为它可能是由于另一个正在运行的进程处于排他模式的页面结构。该结构将是表或索引的主页面。
通常,当通过插入操作在服务器上提高并发请求频率时,这些多个请求将在索引页上使用PAGELATCH_EX等待类型在同一资源上等待。这种情况也称为“热点问题”或“热点”。这种类型的闩锁争用在并发加载时通过更新或删除操作也是可能持有的。
对于此争用,可能是由于顺序的前导索引键导致了问题的产生。通常,建立索引是数据库性能的基础,但可能会引起争用。更具体地说,如果表具有聚簇索引,则在插入数据时会以排序的方式组织数据。尽管它在聚簇索引的末尾添加了一条记录,更直观地说问题出在页面拆分上。但是插入请求队列是在最后一页上生成的,除此之外,我们还可以在集群索引中添加一个标识列,这会导致其他性能问题,而单个对象上的并发插入频率更高。在并发插入时,这些请求如何堆积?图片中如何显示此闩锁争用PAGELATCH_EX?如需更多说明,请查看下图
如图所示,有多个请求将数据插入具有聚集索引的单个表中,因此这些请求在最后一页上等待,因为此插入语句是由物理顺序串行执行的,这种闩锁争用的结果是导致出现过多的PAGELATCH_EX等待。但是,此争用是指最后一页插入争用问题。
确定热闩锁争用问题
为了进一步说明,我将在本地服务器上重新创建此方案,为此,我准备了示例脚本。在脚本中,我介绍了一个基本表和过程以及一个数据库Hotspot。
样例脚本
CREATE DATABASE Hotspot
GO
Use Hotspot
GO
CREATE TABLE audit_Data
(
audit_id int primary key identity (1,1),
audit_action nvarchar(max),
audit_desc nvarchar(max),
ref_person_action int,
actionlist xml,
actionarea xml,
dtauditDate datetime default getdate ()
)
GO
CREATE PROCEDURE audit_history
(
@audit_action nvarchar(MAX),
@audit_desc nvarchar(max),
@ref_person_action int,
@actionlist xml,
@actionarea xml
)
as
begin insert into audit_Data
select @audit_action,@audit_desc,@ref_person_action,@actionlist,@actionarea,GETUTCDATE() select SCOPE_IDENTITY () end GO
在本地服务器上应用负载测试
测试服务器中有多种工具和实用程序可用于负载测试。我使用SQLQuerystress工具。
在此测试过程中,需要获取查询统计信息以解决此问题。出于演示目的,我准备了以下脚本以获取相关信息。
查询获取等待资源与T-SQL执行
我使用了多个SQL DMV,并在此基础上准备了用于获取等待统计信息的查询。另外,我们也可以使用分析器,活动监视器等工具。
SELECT wt.wait_duration_ms ,
wt.wait_type ,
s_text.text ,
DB_NAME(req.database_id) DatabaseName ,
req.wait_resource ,
session.login_name ,
req.last_wait_type
FROM sys.dm_os_waiting_tasks wt
INNER JOIN sys.dm_exec_requests req ON wt.session_id = req.session_id
INNER JOIN sys.dm_exec_sessions session ON session.session_id = req.session_id
CROSS APPLY sys.dm_exec_sql_text(req.sql_handle) s_text
CROSS APPLY sys.dm_exec_query_plan(req.plan_handle) qp
WHERE session.is_user_process = 1;
查询测量的事务统计信息,闩锁等待,批处理请求等。
以下查询以获取锁存器,有关事务与时间的批处理请求。我在此查询中配置了30秒的延迟。
use Hotspot
go
select object_name,counter_name,cntr_value into #perfcounter
From sys.dm_os_performance_counters
where counter_name in
( 'Transactions/sec',
'Latch Waits/sec',
'Average Latch Wait Time (ms)',
'Batch Requests/sec')
and ( ltrim(rtrim(instance_name)) = 'HotSpot'
OR instance_name = '') WAITFOR DELAY '00:00:30'; select counters.object_name,counters.counter_name As ActionName,counters.cntr_value - perf.cntr_value ActionValue
From #perfcounter perf
inner join sys.dm_os_performance_counters counters on counters.counter_name = perf.counter_name
where counters.counter_name in
( 'Transactions/sec',
'Latch Waits/sec',
'Average Latch Wait Time (ms)',
'Batch Requests/sec')
and ( ltrim(rtrim(instance_name)) = 'HotSpot'
OR instance_name = '') DROP TABLE #perfcounter
GO
现在,我可以监视查询以及使用SQLQueryStress工具。现在,我将并行执行这两件事。
- 在本地,我配置了SQLQueryStress,然后应用过程负载100线程* 1000迭代。
- SSMS中都运行了两个监视查询。
SQLQueryStress使用100个线程* 1000次迭代加载测试结果.
在SSMS中监视查询结果
按照上面提到的监视查询,我将在两个不同的会话中执行。
可以看出,在监视查询结果中找到了多个等待类型PAGELATCH_EX的实例。
如何解决热闩锁争用
如果删除聚簇索引,可能可以减少争用,但这可能并不理想。我们有多种方式可以在整个索引范围内分配插入;水平分区技术,使用唯一键列中的哈希值等。
在前导唯一键列中使用哈希值
哈希值是指动态生成的键值。如果我在主键列中使用哈希值,则唯一键值将与audit_id一起分布在B树结构中。因此,我更改了表结构。出于演示目的,我将创建一个表和一个具有不同名称的过程,以下是我附加的脚本。
Use Hotspot
GO
CREATE TABLE audit_Data_with_Hashing
(
audit_id int identity (1,1) NOT NULL,
audit_action nvarchar(max),
audit_desc nvarchar(max),
ref_person_action int,
actionlist xml,
actionarea xml,
dtauditDate datetime default getdate (),
HashValue as (CONVERT([INT], abs([audit_id])%(30))) PERSISTED NOT NULL
)
GO ALTER TABLE audit_Data_with_Hashing
ADD CONSTRAINT pk_hashvalue
PRIMARY KEY CLUSTERED (HashValue, audit_id) --HashValue
GO CREATE PROCEDURE audit_history_with_Hashing ( @audit_action nvarchar(MAX), @audit_desc nvarchar(max), @ref_person_action int, @actionlist xml, @actionarea xml ) as begin insert into audit_Data_with_Hashing select @audit_action,@audit_desc,@ref_person_action,@actionlist,@actionarea,GETUTCDATE() select SCOPE_IDENTITY () end GO
我已经在测试服务器中应用了上述脚本,现在我将应用与以前相同的负载测试并再次捕获统计信息。
具有100个线程* 1000次迭代的SQLQueryStress负载测试结果
我已经重新运行SQLQueryStress工具并执行如下。
在SSMS中监视查询结果
根据前面提到的监视查询,我将在两个不同的会话中执行。
现在,我在30秒内获得了负载测试结果,锁存等待/秒为484,而不是196,746,平均锁存等待时间(MS)数为 206,170,而不是1,421,675。作为查询统计信息的结果,减少了Pagelatch_EX等待。根据用例,这种方法可能会有不同的效果。下面将描述其优缺点。
分区技术
当一个表具有数百万行时,进行DML操作。在这种情况下,需要像垂直和水平表级别分区一样更改结构级别。这些之间需要权衡取舍。水平表分区可以轻松集成。我们还可以在计算列上应用水平分区,该功能几乎与使用领先的唯一索引具有相同的功能,只是差别很小。执行插入操作时,此操作仍在此逻辑范围的末尾,但是哈希值会生成动态值,并且会在B树结构中进行拆分。这样做可能是有可能减少争用,因为可以使用计算列来解决此频率插入争用问题。
结论
我已经总结了要点,并在下面提到了权衡问题。
优点
- 使用聚簇索引,插入将以非顺序的方式执行,并且可以防止闩锁争用问题的发生。
- 表分区功能对于管理大量数据非常有用。
缺点
- 索引键长度大于正常值。由于索引大小的差异和页面遍历的成本,碎片化可能成为一个问题。
- 随机插入操作可能会生成分页操作。
- 如选择查询之类的获取数据操作可能会在从哈希分区检索数据时产生问题,因为查询计划的估计可能不准确。
- 增加索引的键组合时,很难保持引用完整性。