大量INSERT阻止SELECT


14

我遇到大量阻止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)
)

我也有这个小帮助程序,可让我使用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

用法

我现在在多个服务器上运行了服务实例,这些服务器通过[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

结果如下:

在此处输入图片说明

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

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

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

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

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

  • 平均CPU时间:0.12ms
  • 平均读取过程:5.76个/秒
  • 平均写入过程:0.4个/秒

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

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

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

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

这是(所有)完整的sp_WhoIsActive结果集:

SELECT 命令

  • dd hh:mm:ss.mss:00 00:01:01.930
  • session_id:73
  • wait_info:(12629ms)LCK_M_S
  • 处理器:198
  • blocking_session_id:146
  • 读取:99,368
  • 写:0
  • 状态:已暂停
  • open_tran_count:0

封锁[InsertOrUpdateInverterData]指令

  • dd hh:mm:ss.mss:00 00:00:00.330
  • session_id:146
  • wait_info:空
  • 处理器:3,972
  • blocking_session_id:NULL
  • 读取:376,95
  • 写道:126
  • 状态:睡觉
  • open_tran_count:1

([TimeStamp] DESC, [InverterID] ASC)对于聚集索引,看起来像是一个奇怪的选择。我的意思是那DESC部分。
ypercubeᵀᴹ

我理解您的意思:聚簇索引DESC插入数据将迫使表重建,而不是附加到最后。会在重建时锁定表...是的。乔夫(Jove)拥有。结构导致的锁定不仅仅是锁。
Alocyte '17

Answers:


12

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

MERGE [dbo].[InverterData] WITH (SERIALIZABLE) AS [TARGET]

序列化的隔离级别提示可确保将行锁定的键范围已锁定。您具有唯一的索引来支持范围锁定,因此此提示不会对锁定产生不利影响,您仅会获得针对这种潜在竞争条件的保护。

主要问题

为什么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,请务必特别阅读“读取已提交的快照隔离”下的“数据修改”

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


0

谦虚,我不会使用合并。我将使用IF Exists(UPDATE)ELSE(INSERT)-您有一个集群键,其中包含用于标识行的两列,因此这是一个简单的测试。

您提到了MASSIVE插入,但是却是1接1 ...想过将数据分批存储到临时表中,并使用POWER OVERWHELMING SQL数据集一次执行1次以上的更新/插入吗?就像对登台表中的内容进行例行测试,然后一次获取前10000个而不是一次获取...

我会在更新中做这样的事情

DECLARE @Set TABLE (StagingKey, ID,DATE)
INSERT INTO @Set
UPDATE Staging 
SET InProgress = 1
OUTPUT StagingKey, Staging.ID, Staging.Date
WHERE InProgress = 0
AND StagingID IN (SELECT TOP (100000) StagingKey FROM Staging WHERE inProgress = 0 ORDER BY StagingKey ASC ) --FIFO

DECLARE @Temp 
INSERT INTO @TEMP 
UPDATE [DEST] SET Value = Staging.Value [whatever]
OUTPUT INSERTED.ID, DATE [row identifiers]
FROM [DEST] 
JOIN [STAGING]
JOIN [@SET]; 
INSERT INTO @TEMP 
INSERT [DEST] 
SELECT
OUTPUT INSERT.ID, DATE [row identifiers] 
FROM [STAGING] 
JOIN [@SET] 
LEFT JOIN [DEST]

UPDATE Staging
SET inProgress = NULL
FROM Staging 
JOIN @set
ON @Set.Key = Staging.Key
JOIN @temp
ON @temp.id = @set.ID
AND @temp.date = @set.Date

您可能会运行多个作业来弹出更新批处理,并且您需要一个单独的作业来进行细流删除

while exists (inProgress is null) 
delete top (100) from staging where inProgress is null 

清理登台表。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.