大量插入阻止选择

Ste*_*old 15 sql-server-2008 sql-server locking

我遇到了大量阻止我的 SELECT 操作的 INSERT 的问题。

架构

我有一张这样的表:

CREATE TABLE [InverterData](
    [InverterID] [bigint] NOT NULL,
    [TimeStamp] [datetime] NOT NULL,    
    [ValueA] [decimal](18, 2) NULL,
    [ValueB] [decimal](18, 2) NULL
    CONSTRAINT [PrimaryKey_e149e28f-5754-4229-be01-65fafeebce16] PRIMARY KEY CLUSTERED 
    (
        [TimeStamp] DESC,
        [InverterID] ASC
    ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF
    , IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON)
)
Run Code Online (Sandbox Code Playgroud)

我还有这个小助手程序,它允许我使用 MERGE 命令插入或更新(冲突时更新):

CREATE PROCEDURE [InsertOrUpdateInverterData]
    @InverterID bigint, @TimeStamp datetime
    , @ValueA decimal(18,2), @ValueB decimal(18,2)
AS
BEGIN
    MERGE [InverterData] AS TARGET
        USING (VALUES (@InverterID, @TimeStamp, @ValueA, @ValueB))
        AS SOURCE ([InverterID], [TimeStamp], [ValueA], [ValueB])
        ON TARGET.[InverterID] = @InverterID AND TARGET.[TimeStamp] = @TimeStamp
    WHEN MATCHED THEN
        UPDATE
        SET [ValueA] = SOURCE.[ValueA], [ValueB] = SOURCE.[ValueB]              
    WHEN NOT MATCHED THEN
        INSERT ([InverterID], [TimeStamp], [ValueA], [ValueB]) 
        VALUES (SOURCE.[InverterID], SOURCE.[TimeStamp], SOURCE.[ValueA], SOURCE.[ValueB]);
END
Run Code Online (Sandbox Code Playgroud)

用法

我现在已经在多个服务器上运行服务实例,这些服务器通过[InsertOrUpdateInverterData]快速调用过程来执行大量更新。

还有一个网站可以对[InverterData]表进行SELECT 查询。

问题

如果我对[InverterData]表执行 SELECT 查询,它们将在不同的时间跨度中进行,具体取决于我的服务实例的 INSERT 使用情况。如果我暂停所有服务实例,SELECT 会快如闪电,如果实例执行快速插入,则 SELECT 会变得非常慢甚至超时取消。

尝试

我在[sys.dm_tran_locks]表上做了一些 SELECT来查找锁定进程,就像这样

SELECT
tl.request_session_id,
wt.blocking_session_id,
OBJECT_NAME(p.OBJECT_ID) BlockedObjectName,
h1.TEXT AS RequestingText,
h2.TEXT AS BlockingText,
tl.request_mode

FROM sys.dm_tran_locks AS tl

INNER JOIN sys.dm_os_waiting_tasks AS wt ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.partitions AS p ON p.hobt_id = tl.resource_associated_entity_id
INNER JOIN sys.dm_exec_connections ec1 ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2 ON ec2.session_id = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2
Run Code Online (Sandbox Code Playgroud)

这是结果:

在此处输入图片说明

S = 共享。持有会话被授予对资源的共享访问权限。

为什么 SELECT 被[InsertOrUpdateInverterData]仅使用 MERGE 命令的过程阻塞?

我是否必须在内部使用某种具有定义隔离模式的事务[InsertOrUpdateInverterData]

更新 1(与@Paul 的问题有关)

基于 MS-SQL 服务器内部报告有关[InsertOrUpdateInverterData]以下统计信息:

  • 平均 CPU 时间:0.12 毫秒
  • 平均读取进程:5.76 per/s
  • 平均写入进程:0.4 per/s

基于此,看起来 MERGE 命令主要忙于将锁定表的读取操作!(?)

更新 2(与@Paul 的问题有关)

[InverterData]表具有以下存储统计信息:

  • 数据空间:26,901.86 MB
  • 行数:131,827,749
  • 分区:真实
  • 分区数:62

这是(几乎)完整的sp_WhoIsActive结果集:

SELECT 命令

  • dd hh:mm:ss.mss: 00 00:01:01.930
  • session_id:73
  • wait_info: (12629ms)LCK_M_S
  • 中央处理器:198
  • 阻塞会话ID:146
  • 阅读:99,368
  • 写:0
  • 状态:暂停
  • open_tran_count: 0

阻塞[InsertOrUpdateInverterData]命令

  • dd hh:mm:ss.mss: 00 00:00:00.330
  • session_id:146
  • 等待信息:NULL
  • CPU:3,972
  • blocks_session_id: NULL
  • 阅读:376,95
  • 写:126
  • 状态:睡觉
  • open_tran_count: 1

Pau*_*ite 12

首先,虽然与主要问题稍微无关,但您的MERGE陈述可能会因竞争条件而存在错误风险。简而言之,问题在于多个并发线程可能会得出目标行不存在的结论,从而导致插入尝试发生冲突。根本原因是不可能对不存在的行进行共享或更新锁定。解决方法是添加提示:

MERGE [dbo].[InverterData] WITH (SERIALIZABLE) AS [TARGET]
Run Code Online (Sandbox Code Playgroud)

序列化的隔离级别提示确保行所在的键范围被锁定。你有一个唯一的索引来支持范围锁定,所以这个提示不会对锁定产生不利影响,你只会获得针对这种潜在竞争条件的保护。

主要问题

为什么SELECTs只使用MERGE命令的 [InsertOrUpdateInverterData] 过程会阻止?

在默认锁定读提交隔离级别下,读取数据时会使用共享 (S) 锁,并且通常(尽管并非总是)在读取完成后立即释放。一些共享锁被持有到语句的末尾。

MERGE语句修改数据,所以它会获得S或更新(U)锁定位数据到变化,其被转换为当排它(X)锁只是在执行实际修改之前。U 和 X 锁都必须保持到事务结束。

这在所有隔离级别下都是正确的,除了“乐观”快照隔离(SI),不要与版本控制读提交混淆,也称为读提交快照隔离(RCSI)。

您的问题中没有任何内容显示等待 S 锁的会话被持有 U 锁的会话阻塞。这些锁是兼容的。任何阻塞几乎肯定是由对持有的 X 锁的阻塞引起的。当在很短的时间间隔内获取、转换和释放大量短期锁时,这可能有点棘手。

open_tran_count: 1对InsertOrUpdateInverterData命令是值得研究的。尽管该命令运行时间不长,但您应该检查您的包含事务(在应用程序或更高级别的存储过程中)是否过长。最佳做法是使事务尽可能短。这可能没什么,但你一定要检查一下。

潜在的解决方案

正如 Kin 在评论中建议的那样,您可以考虑在此数据库上启用行版本控制隔离级别(RCSI 或 SI)。RCSI 是最常用的,因为它通常不需要那么多的应用程序更改。启用后,默认的已提交读隔离级别使用行版本而不是使用 S 锁进行读取,因此减少或消除了 SX 阻塞。一些操作(例如外键检查)在 RCSI 下仍然需要获取 S 锁。

请注意,行版本会消耗 tempdb 空间,从广义上讲,与更改活动的速率和事务的长度成正比。您需要在负载下彻底测试您的实施以了解和规划 RCSI(或 SI)在您的案例中的影响。

如果您想本地化版本控制的使用,而不是为整个工作负载启用它,SI 可能仍然是更好的选择。通过将 SI 用于读取事务,您将避免读取器和写入器之间的争用,代价是读取器在任何并发修改开始之前看到行的版本(更准确地说,SI 下的读取操作将始终看到已提交的状态) SI 事务开始时的行)。对写事务使用 SI 几乎没有好处,因为仍然会使用写锁,并且您需要处理任何写冲突。除非那是你想要的:)

注意:与 RCSI(一旦启用适用于所有在读提交时运行的事务)不同,SI 必须使用SET TRANSACTION ISOLATION SNAPSHOT;.

依赖于阻止作者的读者(包括在触发器代码中!)的微妙行为使测试变得必不可少。有关详细信息,请参阅我的链接文章系列和在线书籍。如果您决定使用 RCSI,请务必特别查看 Read Committed Snapshot Isolation 下的 Data Modifications

最后,您应该确保您的实例已修补到 SQL Server 2008 Service Pack 4。